Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
257 lines
8.7 KiB
Swift
257 lines
8.7 KiB
Swift
//
|
||
// 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
|
||
}
|
||
}
|