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:
2026-06-24 11:08:41 +08:00
parent c772b25897
commit 0d4b63d273
22 changed files with 2698 additions and 12 deletions

View 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 ""
}
/// StringDouble 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
}
/// StringDouble 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
}
}