// // PunchPointModels.swift // suixinkan // // Created by Codex on 2026/6/24. // import Foundation /// 打卡点筛选实体,表示列表页可选择的审核或运营状态。 enum PunchPointFilter: Int, CaseIterable, Identifiable { case all = 0 case operating = 1 case paused = 2 case pendingReview = 3 case rejected = 4 var id: Int { rawValue } /// 筛选项展示标题。 var title: String { switch self { case .all: "全部" case .operating: "运营中" case .paused: "已暂停" case .pendingReview: "待审核" case .rejected: "已驳回" } } } /// 打卡点区域实体,表示点位经纬度和地址。 struct PunchPointRegion: Codable, Hashable { var lat: Double var lot: Double var address: String var scenicSpotStr: String? enum CodingKeys: String, CodingKey { case lat case lot case address case scenicSpotStr = "scenic_spot_str" } /// 创建打卡点区域实体。 init(lat: Double, lot: Double, address: String, scenicSpotStr: String? = nil) { self.lat = lat self.lot = lot self.address = address self.scenicSpotStr = scenicSpotStr } /// 宽松解码区域字段,兼容经纬度数字和字符串混合返回。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) lat = try container.decodeLossyDouble(forKey: .lat) ?? 0 lot = try container.decodeLossyDouble(forKey: .lot) ?? 0 address = try container.decodeLossyString(forKey: .address) scenicSpotStr = try container.decodeIfPresent(String.self, forKey: .scenicSpotStr) } } /// 打卡点列表项实体,表示一个景区下可管理的打卡点。 struct PunchPointItem: Decodable, Hashable, Identifiable { let id: Int let scenicAreaId: Int let name: String let status: Int let statusLabel: String let description: String let region: PunchPointRegion? let scenicSpotStr: String let guideImages: [String] let mpQrcode: String let createdAt: String let creator: String let creatorPhone: String let auditor: String let auditTime: String let auditRemark: String enum CodingKeys: String, CodingKey { case id case scenicAreaId = "scenic_area_id" case name case status case statusLabel = "status_label" case description case region case scenicSpotStr = "scenic_spot_str" case guideImages = "guide_imgs" case mpQrcode = "mp_qrcode" case createdAt = "created_at" case creator case creatorPhone = "creator_phone" case auditor case auditTime = "audit_time" case auditRemark = "audit_remark" } /// 创建打卡点列表项,主要用于测试和表单兜底展示。 init( id: Int, scenicAreaId: Int = 0, name: String, status: Int = 0, statusLabel: String = "", description: String = "", region: PunchPointRegion? = nil, scenicSpotStr: String = "", guideImages: [String] = [], mpQrcode: String = "", createdAt: String = "", creator: String = "", creatorPhone: String = "", auditor: String = "", auditTime: String = "", auditRemark: String = "" ) { self.id = id self.scenicAreaId = scenicAreaId self.name = name self.status = status self.statusLabel = statusLabel self.description = description self.region = region self.scenicSpotStr = scenicSpotStr self.guideImages = guideImages self.mpQrcode = mpQrcode self.createdAt = createdAt self.creator = creator self.creatorPhone = creatorPhone self.auditor = auditor self.auditTime = auditTime self.auditRemark = auditRemark } /// 宽松解码打卡点字段,兼容后端字符串和数字混合返回。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 scenicAreaId = try container.decodeLossyInt(forKey: .scenicAreaId) ?? 0 name = try container.decodeLossyString(forKey: .name) status = try container.decodeLossyInt(forKey: .status) ?? 0 statusLabel = try container.decodeLossyString(forKey: .statusLabel) description = try container.decodeLossyString(forKey: .description) region = try container.decodeIfPresent(PunchPointRegion.self, forKey: .region) scenicSpotStr = try container.decodeLossyString(forKey: .scenicSpotStr) guideImages = (try? container.decodeIfPresent([String].self, forKey: .guideImages)) ?? [] mpQrcode = try container.decodeLossyString(forKey: .mpQrcode) createdAt = try container.decodeLossyString(forKey: .createdAt) creator = try container.decodeLossyString(forKey: .creator) creatorPhone = try container.decodeLossyString(forKey: .creatorPhone) auditor = try container.decodeLossyString(forKey: .auditor) auditTime = try container.decodeLossyString(forKey: .auditTime) auditRemark = try container.decodeLossyString(forKey: .auditRemark) } } /// 新增打卡点请求实体,表示提交给后端的点位表单。 struct AddPunchPointRequest: Encodable, Equatable { let scenicAreaId: String let name: String let description: String let region: PunchPointRegion let scenicSpotStr: String let guideImages: [String] enum CodingKeys: String, CodingKey { case scenicAreaId = "scenic_area_id" case name case description case region case scenicSpotStr = "scenic_spot_str" case guideImages = "guide_imgs" } } /// 编辑打卡点请求实体,表示已有点位的完整修改内容。 struct EditPunchPointRequest: Encodable, Equatable { let id: Int let scenicAreaId: String let name: String let description: String let region: PunchPointRegion let scenicSpotStr: String let guideImages: [String] enum CodingKeys: String, CodingKey { case id case scenicAreaId = "scenic_area_id" case name case description case region case scenicSpotStr = "scenic_spot_str" case guideImages = "guide_imgs" } } /// 删除打卡点请求实体,表示要删除的点位 ID。 struct PunchPointDeleteRequest: Encodable, Equatable { let id: Int } /// 打卡点本地待上传图片实体,保存 PhotosPicker 读取后的内存图片。 struct PunchPointLocalImage: Identifiable, Equatable { let id = UUID() let data: Data let fileName: String var remoteURL: String? var progress: Int /// 创建待上传打卡点图片。 init(data: Data, fileName: String, remoteURL: String? = nil, progress: Int = 0) { self.data = data self.fileName = fileName self.remoteURL = remoteURL self.progress = progress } } 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 ? "1" : "0" } return "" } /// 将 String、Double 和 Int 宽松解码为 Int。 func decodeLossyInt(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) { return Int(value.trimmingCharacters(in: .whitespacesAndNewlines)) } return nil } /// 将 String、Double 和 Int 宽松解码为 Double。 func decodeLossyDouble(forKey key: Key) throws -> Double? { if let value = try? decodeIfPresent(Double.self, forKey: key) { return value } if let value = try? decodeIfPresent(Int.self, forKey: key) { return Double(value) } if let value = try? decodeIfPresent(String.self, forKey: key) { return Double(value.trimmingCharacters(in: .whitespacesAndNewlines)) } return nil } }