// // PunchPointModels.swift // suixinkan // import CoreLocation import Foundation /// 打卡点列表筛选类型,对齐 Android `PunchPointFilterType`。 enum PunchPointFilterType: Int, CaseIterable, Sendable, Equatable, Hashable { case all = 0 case inOperation = 1 case paused = 2 case pendingReview = 3 case rejected = 4 /// 页面展示文案。 var title: String { switch self { case .all: "全部" case .inOperation: "运营中" case .paused: "已暂停" case .pendingReview: "未审核" case .rejected: "审核未通过" } } /// 服务端 `status` 参数。 var apiStatus: Int { rawValue } } /// 打卡点列表响应。 struct PunchPointListResponse: Decodable, Sendable, Equatable { let total: Int let list: [PunchPointItem] init(total: Int = 0, list: [PunchPointItem] = []) { self.total = total self.list = list } } /// 打卡点经纬度与地址区域。 struct PunchPointRegion: Codable, Sendable, Equatable, Hashable { let lat: Double let lot: Double let address: String let scenicSpotString: String? enum CodingKeys: String, CodingKey { case lat case lot case address case scenicSpotString = "scenic_spot_str" } init(lat: Double = 0, lot: Double = 0, address: String = "", scenicSpotString: String? = nil) { self.lat = lat self.lot = lot self.address = address self.scenicSpotString = scenicSpotString } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) lat = try container.decodeIfPresent(Double.self, forKey: .lat) ?? 0 lot = try container.decodeIfPresent(Double.self, forKey: .lot) ?? 0 address = try container.decodeIfPresent(String.self, forKey: .address) ?? "" scenicSpotString = try container.decodeIfPresent(String.self, forKey: .scenicSpotString) } } /// 打卡点列表条目,对齐 Android `ScenicSpotItem`。 struct PunchPointItem: Decodable, Sendable, Equatable, Hashable, Identifiable { let id: Int64 let name: String let status: Int let statusLabel: String let region: PunchPointRegion? let scenicSpotString: String let guideImages: [String] let mpQrcode: String? let createdAt: String let creatorId: Int64? let creator: String? let creatorPhone: String? let auditor: String? let auditTime: String? let auditRemark: String? enum CodingKeys: String, CodingKey { case id case name case status case statusLabel = "status_label" case region case scenicSpotString = "scenic_spot_str" case guideImages = "guide_imgs" case mpQrcode = "mp_qrcode" case createdAt = "created_at" case creatorId = "creator_id" case creator case creatorPhone = "creator_phone" case auditor case auditTime = "audit_time" case auditRemark = "audit_remark" } init( id: Int64 = 0, name: String = "", status: Int = 0, statusLabel: String = "", region: PunchPointRegion? = nil, scenicSpotString: String = "", guideImages: [String] = [], mpQrcode: String? = nil, createdAt: String = "", creatorId: Int64? = nil, creator: String? = nil, creatorPhone: String? = nil, auditor: String? = nil, auditTime: String? = nil, auditRemark: String? = nil ) { self.id = id self.name = name self.status = status self.statusLabel = statusLabel self.region = region self.scenicSpotString = scenicSpotString self.guideImages = guideImages self.mpQrcode = mpQrcode self.createdAt = createdAt self.creatorId = creatorId 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.decodeFlexibleInt64(forKey: .id) ?? 0 name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" status = try container.decodeIfPresent(Int.self, forKey: .status) ?? 0 statusLabel = try container.decodeIfPresent(String.self, forKey: .statusLabel) ?? "" region = try container.decodeIfPresent(PunchPointRegion.self, forKey: .region) scenicSpotString = try container.decodeIfPresent(String.self, forKey: .scenicSpotString) ?? "" guideImages = try container.decodeIfPresent([String].self, forKey: .guideImages) ?? [] mpQrcode = try container.decodeIfPresent(String.self, forKey: .mpQrcode) createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? "" creatorId = try container.decodeFlexibleInt64(forKey: .creatorId) creator = try container.decodeIfPresent(String.self, forKey: .creator) creatorPhone = try container.decodeIfPresent(String.self, forKey: .creatorPhone) auditor = try container.decodeIfPresent(String.self, forKey: .auditor) auditTime = try container.decodeIfPresent(String.self, forKey: .auditTime) auditRemark = try container.decodeIfPresent(String.self, forKey: .auditRemark) } /// 列表或详情展示地址。 var displayAddress: String { let address = region?.address.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if !address.isEmpty { return address } return scenicSpotString.trimmingCharacters(in: .whitespacesAndNewlines) } } /// 打卡点详情响应,对齐 Android `ScenicSpotDetailResponse`。 struct PunchPointDetail: Decodable, Sendable, Equatable { let id: Int64 let name: String let status: Int let statusLabel: String let region: PunchPointRegion? let scenicSpotString: String let description: String? let guideImages: [String] let mpQrcode: String? let createdAt: String let creatorId: Int64? let creator: String? let creatorPhone: String? let auditor: String? let auditTime: String? let auditRemark: String? enum CodingKeys: String, CodingKey { case id case name case status case statusLabel = "status_label" case region case scenicSpotString = "scenic_spot_str" case description case guideImages = "guide_imgs" case mpQrcode = "mp_qrcode" case createdAt = "created_at" case creatorId = "creator_id" case creator case creatorPhone = "creator_phone" case auditor case auditTime = "audit_time" case auditRemark = "audit_remark" } init( id: Int64 = 0, name: String = "", status: Int = 0, statusLabel: String = "", region: PunchPointRegion? = nil, scenicSpotString: String = "", description: String? = nil, guideImages: [String] = [], mpQrcode: String? = nil, createdAt: String = "", creatorId: Int64? = nil, creator: String? = nil, creatorPhone: String? = nil, auditor: String? = nil, auditTime: String? = nil, auditRemark: String? = nil ) { self.id = id self.name = name self.status = status self.statusLabel = statusLabel self.region = region self.scenicSpotString = scenicSpotString self.description = description self.guideImages = guideImages self.mpQrcode = mpQrcode self.createdAt = createdAt self.creatorId = creatorId 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.decodeFlexibleInt64(forKey: .id) ?? 0 name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" status = try container.decodeIfPresent(Int.self, forKey: .status) ?? 0 statusLabel = try container.decodeIfPresent(String.self, forKey: .statusLabel) ?? "" region = try container.decodeIfPresent(PunchPointRegion.self, forKey: .region) scenicSpotString = try container.decodeIfPresent(String.self, forKey: .scenicSpotString) ?? "" description = try container.decodeIfPresent(String.self, forKey: .description) guideImages = try container.decodeIfPresent([String].self, forKey: .guideImages) ?? [] mpQrcode = try container.decodeIfPresent(String.self, forKey: .mpQrcode) createdAt = try container.decodeIfPresent(String.self, forKey: .createdAt) ?? "" creatorId = try container.decodeFlexibleInt64(forKey: .creatorId) creator = try container.decodeIfPresent(String.self, forKey: .creator) creatorPhone = try container.decodeIfPresent(String.self, forKey: .creatorPhone) auditor = try container.decodeIfPresent(String.self, forKey: .auditor) auditTime = try container.decodeIfPresent(String.self, forKey: .auditTime) auditRemark = try container.decodeIfPresent(String.self, forKey: .auditRemark) } /// 详情展示地址。 var displayAddress: String { let address = region?.address.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if !address.isEmpty { return address } return scenicSpotString.trimmingCharacters(in: .whitespacesAndNewlines) } /// 编辑页初始化坐标。 var coordinate: CLLocationCoordinate2D? { guard let region else { return nil } return CLLocationCoordinate2D(latitude: region.lat, longitude: region.lot) } } /// 新建或编辑打卡点请求体。 struct PunchPointSaveRequest: Encodable, Sendable, Equatable { let id: Int64? let scenicAreaId: String let name: String let description: String? let region: PunchPointSaveRegion let scenicSpotString: String let guideImages: [String] enum CodingKeys: String, CodingKey { case id case scenicAreaId = "scenic_area_id" case name case description case region case scenicSpotString = "scenic_spot_str" case guideImages = "guide_imgs" } init( id: Int64? = nil, scenicAreaId: String, name: String, description: String?, region: PunchPointSaveRegion, scenicSpotString: String, guideImages: [String] ) { self.id = id self.scenicAreaId = scenicAreaId self.name = name self.description = description self.region = region self.scenicSpotString = scenicSpotString self.guideImages = guideImages } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(id, forKey: .id) try container.encode(scenicAreaId, forKey: .scenicAreaId) try container.encode(name, forKey: .name) try container.encodeIfPresent(description, forKey: .description) try container.encode(region, forKey: .region) try container.encode(scenicSpotString, forKey: .scenicSpotString) try container.encode(guideImages, forKey: .guideImages) } } /// 新建或编辑打卡点中的坐标请求体。 struct PunchPointSaveRegion: Encodable, Sendable, Equatable { let lot: Double let lat: Double let address: String } /// 删除打卡点请求体。 struct PunchPointIDRequest: Encodable, Sendable, Equatable { let id: Int64 } /// 打卡点图片上传状态。 struct PunchPointImageState: Sendable, Equatable, Hashable, Identifiable { let id: UUID var data: Data var previewURL: String? var fileName: String var uploadedURL: String var isUploading: Bool var uploadProgress: Int var errorMessage: String? init( id: UUID = UUID(), data: Data, previewURL: String? = nil, fileName: String, uploadedURL: String = "", isUploading: Bool = false, uploadProgress: Int = 0, errorMessage: String? = nil ) { self.id = id self.data = data self.previewURL = previewURL self.fileName = fileName self.uploadedURL = uploadedURL self.isUploading = isUploading self.uploadProgress = uploadProgress self.errorMessage = errorMessage } /// 已上传网络图片状态。 static func remote(url: String) -> PunchPointImageState { PunchPointImageState( data: Data(), previewURL: url, fileName: URL(string: url)?.lastPathComponent.nonEmptyTrimmed ?? "punch_point.jpg", uploadedURL: url, isUploading: false, uploadProgress: 100 ) } /// 是否已经上传完成。 var isUploaded: Bool { !uploadedURL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } } /// 打卡点上传进度弹窗状态。 struct PunchPointUploadDialogState: Sendable, Equatable { let title: String let progress: Int } /// 打卡点 UI 文案与格式化工具。 enum PunchPointDisplayFormatter { /// 手机号脱敏。 static func maskedPhone(_ phone: String?) -> String { guard let phone = phone?.trimmingCharacters(in: .whitespacesAndNewlines), !phone.isEmpty else { return "--" } guard phone.count >= 11 else { return phone } let prefix = phone.prefix(3) let suffix = phone.suffix(phone.count - 7) return "\(prefix)****\(suffix)" } /// 审核/运营标签背景色。 static func statusColors(_ status: Int) -> (background: UInt, text: UInt) { switch status { case 1: return (0xF0FDF4, 0x09BE4F) case 2: return (0xFFF0E2, 0xFF7B00) case 3: return (0xEFF6FF, 0x0073FF) case 4: return (0xFFE7E7, 0xEF4444) default: return (0xF4F4F4, 0x666666) } } } private extension KeyedDecodingContainer { func decodeFlexibleInt64(forKey key: Key) throws -> Int64? { if let value = try decodeIfPresent(Int64.self, forKey: key) { return value } if let value = try decodeIfPresent(Int.self, forKey: key) { return Int64(value) } if let value = try decodeIfPresent(String.self, forKey: key) { return Int64(value) } return nil } } private extension String { var nonEmptyTrimmed: String? { let text = trimmingCharacters(in: .whitespacesAndNewlines) return text.isEmpty ? nil : text } }