Add PunchPoint and LocationReport modules with home routing.
Migrate check-in point management and location reporting from placeholders to full MVVM flows, including foreground location support and unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
252
suixinkan/Features/PunchPoint/Models/PunchPointModels.swift
Normal file
252
suixinkan/Features/PunchPoint/Models/PunchPointModels.swift
Normal file
@ -0,0 +1,252 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user