Add Projects, Schedule, and Invite modules with home routing.
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>
This commit is contained in:
440
suixinkan/Features/Projects/Models/ProjectModels.swift
Normal file
440
suixinkan/Features/Projects/Models/ProjectModels.swift
Normal file
@ -0,0 +1,440 @@
|
||||
//
|
||||
// ProjectModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 项目详情实体,表示摄影师项目或店铺项目详情页需要展示的完整业务字段。
|
||||
struct PhotographerProjectDetailResponse: Decodable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let scenicId: Int
|
||||
let cover: String
|
||||
let coverProject: String
|
||||
let coverCarousel: [String]
|
||||
let name: String
|
||||
let type: Int
|
||||
let typeName: String
|
||||
let status: Int
|
||||
let statusName: String
|
||||
let price: String
|
||||
let otPrice: String
|
||||
let priceDeposit: String
|
||||
let description: String
|
||||
let attrLabel: [String]
|
||||
let projectContent: String
|
||||
let sold: Int
|
||||
let unitName: String
|
||||
let materialNum: Int
|
||||
let photoNum: Int
|
||||
let videoNum: Int
|
||||
let createdAt: String
|
||||
let updatedAt: String
|
||||
let creatorName: String
|
||||
let creatorPhone: String
|
||||
let creatorRoleName: String
|
||||
let auditRemark: String
|
||||
let photogList: [ProjectPhotographerBrief]
|
||||
let scenicList: [ProjectScenicSpotBrief]
|
||||
|
||||
/// 字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case scenicId = "scenic_id"
|
||||
case cover
|
||||
case coverProject = "cover_project"
|
||||
case coverCarousel = "cover_carousel"
|
||||
case name
|
||||
case type
|
||||
case typeName = "type_name"
|
||||
case status
|
||||
case statusName = "status_name"
|
||||
case price
|
||||
case otPrice = "ot_price"
|
||||
case priceDeposit = "price_deposit"
|
||||
case description
|
||||
case attrLabel = "attr_label"
|
||||
case projectContent = "project_content"
|
||||
case sold
|
||||
case unitName = "unit_name"
|
||||
case materialNum = "material_num"
|
||||
case photoNum = "photo_num"
|
||||
case videoNum = "video_num"
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case creatorName = "creator_name"
|
||||
case creatorPhone = "creator_phone"
|
||||
case creatorRoleName = "creator_role_name"
|
||||
case auditRemark = "audit_remark"
|
||||
case photogList = "photog_list"
|
||||
case scenicList = "scenic_list"
|
||||
}
|
||||
|
||||
/// 宽松解码项目详情字段。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeProjectLossyInt(forKey: .id) ?? 0
|
||||
scenicId = try container.decodeProjectLossyInt(forKey: .scenicId) ?? 0
|
||||
cover = try container.decodeProjectLossyString(forKey: .cover)
|
||||
coverProject = try container.decodeProjectLossyString(forKey: .coverProject)
|
||||
coverCarousel = try container.decodeProjectLossyStringArray(forKey: .coverCarousel)
|
||||
name = try container.decodeProjectLossyString(forKey: .name)
|
||||
type = try container.decodeProjectLossyInt(forKey: .type) ?? 0
|
||||
typeName = try container.decodeProjectLossyString(forKey: .typeName)
|
||||
status = try container.decodeProjectLossyInt(forKey: .status) ?? 0
|
||||
statusName = try container.decodeProjectLossyString(forKey: .statusName)
|
||||
price = try container.decodeProjectLossyString(forKey: .price)
|
||||
otPrice = try container.decodeProjectLossyString(forKey: .otPrice)
|
||||
priceDeposit = try container.decodeProjectLossyString(forKey: .priceDeposit)
|
||||
description = try container.decodeProjectLossyString(forKey: .description)
|
||||
attrLabel = try container.decodeProjectLossyStringArray(forKey: .attrLabel)
|
||||
projectContent = try container.decodeProjectLossyString(forKey: .projectContent)
|
||||
sold = try container.decodeProjectLossyInt(forKey: .sold) ?? 0
|
||||
unitName = try container.decodeProjectLossyString(forKey: .unitName)
|
||||
materialNum = try container.decodeProjectLossyInt(forKey: .materialNum) ?? 0
|
||||
photoNum = try container.decodeProjectLossyInt(forKey: .photoNum) ?? 0
|
||||
videoNum = try container.decodeProjectLossyInt(forKey: .videoNum) ?? 0
|
||||
createdAt = try container.decodeProjectLossyString(forKey: .createdAt)
|
||||
updatedAt = try container.decodeProjectLossyString(forKey: .updatedAt)
|
||||
creatorName = try container.decodeProjectLossyString(forKey: .creatorName)
|
||||
creatorPhone = try container.decodeProjectLossyString(forKey: .creatorPhone)
|
||||
creatorRoleName = try container.decodeProjectLossyString(forKey: .creatorRoleName)
|
||||
auditRemark = try container.decodeProjectLossyString(forKey: .auditRemark)
|
||||
photogList = (try? container.decode([ProjectPhotographerBrief].self, forKey: .photogList)) ?? []
|
||||
scenicList = (try? container.decode([ProjectScenicSpotBrief].self, forKey: .scenicList)) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// 项目摄影师摘要实体,表示项目详情中的摄影师信息。
|
||||
struct ProjectPhotographerBrief: Decodable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let photogUid: Int
|
||||
let nickname: String
|
||||
let avatar: String
|
||||
let orderNum: Int
|
||||
let completedOrderCount: Int
|
||||
|
||||
/// 字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case photogUid = "photog_uid"
|
||||
case nickname
|
||||
case avatar
|
||||
case orderNum = "order_num"
|
||||
case completedOrderCount = "completed_order_count"
|
||||
}
|
||||
|
||||
/// 宽松解码摄影师摘要。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeProjectLossyInt(forKey: .id) ?? 0
|
||||
photogUid = try container.decodeProjectLossyInt(forKey: .photogUid) ?? 0
|
||||
nickname = try container.decodeProjectLossyString(forKey: .nickname)
|
||||
avatar = try container.decodeProjectLossyString(forKey: .avatar)
|
||||
orderNum = try container.decodeProjectLossyInt(forKey: .orderNum) ?? 0
|
||||
completedOrderCount = try container.decodeProjectLossyInt(forKey: .completedOrderCount) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
/// 项目打卡点摘要实体,表示项目关联的景点或打卡点。
|
||||
struct ProjectScenicSpotBrief: Decodable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let name: String
|
||||
|
||||
/// 字段映射。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
}
|
||||
|
||||
/// 宽松解码打卡点摘要。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeProjectLossyInt(forKey: .id) ?? 0
|
||||
name = try container.decodeProjectLossyString(forKey: .name)
|
||||
}
|
||||
}
|
||||
|
||||
/// 摄影师项目创建或编辑请求实体。
|
||||
struct ProjectCreateRequest: Encodable, Equatable {
|
||||
let id: Int?
|
||||
let type: Int
|
||||
let scenicId: Int
|
||||
let name: String
|
||||
let price: String
|
||||
let otPrice: String?
|
||||
let coverProject: String
|
||||
let coverCarousel: [String]?
|
||||
let description: String
|
||||
let attrLabel: [String]?
|
||||
let extra: ProjectCreateExtra
|
||||
let allowNFC: Int = 1
|
||||
|
||||
/// 请求字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case type
|
||||
case scenicId = "scenic_id"
|
||||
case name
|
||||
case price
|
||||
case otPrice = "ot_price"
|
||||
case coverProject = "cover_project"
|
||||
case coverCarousel = "cover_carousel"
|
||||
case description
|
||||
case attrLabel = "attr_label"
|
||||
case extra
|
||||
case allowNFC = "allow_nfc"
|
||||
}
|
||||
}
|
||||
|
||||
/// 摄影师项目扩展配置实体,表示交付数量、押金和关联打卡点/摄影师。
|
||||
struct ProjectCreateExtra: Encodable, Equatable {
|
||||
let priceDeposit: String
|
||||
let materialNum: Int
|
||||
let photoNum: Int
|
||||
let videoNum: Int
|
||||
let scenicSpotId: [Int]
|
||||
let photogUid: [Int]
|
||||
|
||||
/// 请求字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case priceDeposit = "price_deposit"
|
||||
case materialNum = "material_num"
|
||||
case photoNum = "photo_num"
|
||||
case videoNum = "video_num"
|
||||
case scenicSpotId = "scenic_spot_id"
|
||||
case photogUid = "photog_uid"
|
||||
}
|
||||
}
|
||||
|
||||
/// 项目删除请求实体。
|
||||
struct ProjectDeleteRequest: Encodable, Equatable {
|
||||
let id: Int
|
||||
}
|
||||
|
||||
/// 店铺项目可管理景区实体。
|
||||
struct StoreManagerScenicItem: Decodable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let name: String
|
||||
|
||||
/// 字段映射。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
}
|
||||
|
||||
/// 宽松解码店铺项目景区。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeProjectLossyInt(forKey: .id) ?? 0
|
||||
name = try container.decodeProjectLossyString(forKey: .name)
|
||||
}
|
||||
}
|
||||
|
||||
/// 店铺项目套餐实体,表示多点位项目的套餐价格配置。
|
||||
struct StoreManagerPackageItem: Codable, Hashable {
|
||||
let materialNum: Int
|
||||
let price: String
|
||||
|
||||
/// 请求字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case materialNum = "material_num"
|
||||
case price
|
||||
}
|
||||
}
|
||||
|
||||
/// 店铺项目创建请求实体,用于多点位项目。
|
||||
struct StoreManagerCreateRequest: Encodable, Equatable {
|
||||
let name: String
|
||||
let storeId: Int?
|
||||
let type: Int
|
||||
let description: String
|
||||
let coverProject: String?
|
||||
let coverCarousel: [String]?
|
||||
let projectRule: String?
|
||||
let scenicId: [Int]
|
||||
let settleSpotNum: Int
|
||||
let price: Double
|
||||
let priceMaterial: Double
|
||||
let pricePhoto: Double
|
||||
let priceVideo: Double
|
||||
let priceMaterialAll: Double?
|
||||
let packageList: [StoreManagerPackageItem]?
|
||||
let userId: Int
|
||||
let singleSpotMaterialNum: Int
|
||||
let singleSpotPhotoNum: Int
|
||||
let singleSpotVideoNum: Int
|
||||
|
||||
/// 请求字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case storeId = "store_id"
|
||||
case type
|
||||
case description
|
||||
case coverProject = "cover_project"
|
||||
case coverCarousel = "cover_carousel"
|
||||
case projectRule = "project_rule"
|
||||
case scenicId = "scenic_id"
|
||||
case settleSpotNum = "settle_spot_num"
|
||||
case price
|
||||
case priceMaterial = "price_material"
|
||||
case pricePhoto = "price_photo"
|
||||
case priceVideo = "price_video"
|
||||
case priceMaterialAll = "price_material_all"
|
||||
case packageList = "package"
|
||||
case userId = "user_id"
|
||||
case singleSpotMaterialNum = "single_spot_material_num"
|
||||
case singleSpotPhotoNum = "single_spot_photo_num"
|
||||
case singleSpotVideoNum = "single_spot_video_num"
|
||||
}
|
||||
}
|
||||
|
||||
/// 店铺项目更新请求实体,用于多点位项目。
|
||||
struct StoreManagerUpdateRequest: Encodable, Equatable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let type: Int
|
||||
let description: String
|
||||
let coverProject: String?
|
||||
let coverCarousel: [String]?
|
||||
let projectRule: String?
|
||||
let scenicId: [Int]
|
||||
let settleSpotNum: Int
|
||||
let price: Double
|
||||
let priceMaterial: Double
|
||||
let pricePhoto: Double
|
||||
let priceVideo: Double
|
||||
let priceMaterialAll: Double?
|
||||
let packageList: [StoreManagerPackageItem]?
|
||||
let userId: Int
|
||||
let singleSpotMaterialNum: Int
|
||||
let singleSpotPhotoNum: Int
|
||||
let singleSpotVideoNum: Int
|
||||
|
||||
/// 请求字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
case type
|
||||
case description
|
||||
case coverProject = "cover_project"
|
||||
case coverCarousel = "cover_carousel"
|
||||
case projectRule = "project_rule"
|
||||
case scenicId = "scenic_id"
|
||||
case settleSpotNum = "settle_spot_num"
|
||||
case price
|
||||
case priceMaterial = "price_material"
|
||||
case pricePhoto = "price_photo"
|
||||
case priceVideo = "price_video"
|
||||
case priceMaterialAll = "price_material_all"
|
||||
case packageList = "package"
|
||||
case userId = "user_id"
|
||||
case singleSpotMaterialNum = "single_spot_material_num"
|
||||
case singleSpotPhotoNum = "single_spot_photo_num"
|
||||
case singleSpotVideoNum = "single_spot_video_num"
|
||||
}
|
||||
}
|
||||
|
||||
/// 店铺押金项目创建请求实体。
|
||||
struct StoreManagerOfflineCreateRequest: Encodable, Equatable {
|
||||
let name: String
|
||||
let scenicId: Int
|
||||
let storeId: Int
|
||||
let description: String
|
||||
let price: Double
|
||||
let coverProject: String
|
||||
let coverCarousel: [String]
|
||||
|
||||
/// 请求字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case scenicId = "scenic_id"
|
||||
case storeId = "store_id"
|
||||
case description
|
||||
case price
|
||||
case coverProject = "cover_project"
|
||||
case coverCarousel = "cover_carousel"
|
||||
}
|
||||
}
|
||||
|
||||
/// 店铺押金项目更新请求实体。
|
||||
struct StoreManagerOfflineUpdateRequest: Encodable, Equatable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let scenicId: Int
|
||||
let description: String
|
||||
let price: Double
|
||||
let coverProject: String?
|
||||
let coverCarousel: [String]?
|
||||
let storeId: Int?
|
||||
|
||||
/// 请求字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
case scenicId = "scenic_id"
|
||||
case description
|
||||
case price
|
||||
case coverProject = "cover_project"
|
||||
case coverCarousel = "cover_carousel"
|
||||
case storeId = "store_id"
|
||||
}
|
||||
}
|
||||
|
||||
/// 项目本地待上传图片实体,保存 PhotosPicker 读取后的图片数据。
|
||||
struct ProjectLocalImage: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
let data: Data
|
||||
let fileName: String
|
||||
let previewURL: String?
|
||||
}
|
||||
|
||||
private extension KeyedDecodingContainer {
|
||||
/// 将 String、数字和 Bool 宽松解码为字符串。
|
||||
func decodeProjectLossyString(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 decodeProjectLossyInt(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
|
||||
}
|
||||
|
||||
/// 将数组或逗号分隔字符串宽松解码为字符串数组。
|
||||
func decodeProjectLossyStringArray(forKey key: Key) throws -> [String] {
|
||||
if let values = try? decodeIfPresent([String].self, forKey: key) {
|
||||
return values
|
||||
}
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
return value
|
||||
.components(separatedBy: CharacterSet(charactersIn: ",,\n"))
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user