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>
507 lines
17 KiB
Swift
507 lines
17 KiB
Swift
//
|
||
// ScenicQueueModels.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/25.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// 排队统计响应。
|
||
struct ScenicQueueStatsData: Decodable, Equatable {
|
||
let queueCount: Int
|
||
let avgWaitMin: Double
|
||
let time: String
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case queueCount = "queue_count"
|
||
case avgWaitMin = "avg_wait_min"
|
||
case time
|
||
}
|
||
|
||
/// 创建排队统计,主要用于空响应兜底。
|
||
init(queueCount: Int = 0, avgWaitMin: Double = 0, time: String = "") {
|
||
self.queueCount = queueCount
|
||
self.avgWaitMin = avgWaitMin
|
||
self.time = time
|
||
}
|
||
|
||
/// 宽松解码统计字段。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
queueCount = try container.decodeLossyInt(forKey: .queueCount) ?? 0
|
||
avgWaitMin = try container.decodeLossyDouble(forKey: .avgWaitMin) ?? 0
|
||
time = try container.decodeLossyString(forKey: .time)
|
||
}
|
||
}
|
||
|
||
/// 排队首页响应,兼容 list 为数组或分页对象两种形态。
|
||
struct ScenicQueueHomeData: Decodable, Equatable {
|
||
let stats: ScenicQueueHomeStats?
|
||
let list: ScenicQueueHomeListBlock?
|
||
let time: String?
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case stats
|
||
case list
|
||
case time
|
||
}
|
||
|
||
/// 创建排队首页数据,主要用于空响应兜底。
|
||
init(stats: ScenicQueueHomeStats? = nil, list: ScenicQueueHomeListBlock? = nil, time: String? = nil) {
|
||
self.stats = stats
|
||
self.list = list
|
||
self.time = time
|
||
}
|
||
|
||
/// 宽松解码排队首页数据。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
stats = try container.decodeIfPresent(ScenicQueueHomeStats.self, forKey: .stats)
|
||
if let block = try? container.decodeIfPresent(ScenicQueueHomeListBlock.self, forKey: .list) {
|
||
list = block
|
||
} else if let tickets = try? container.decodeIfPresent([ScenicQueueTicket].self, forKey: .list) {
|
||
list = ScenicQueueHomeListBlock(list: tickets)
|
||
} else {
|
||
list = nil
|
||
}
|
||
time = try container.decodeIfPresent(String.self, forKey: .time)
|
||
}
|
||
}
|
||
|
||
/// 排队首页统计块。
|
||
struct ScenicQueueHomeStats: Decodable, Equatable {
|
||
let type: Int
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case type
|
||
}
|
||
|
||
/// 创建统计块。
|
||
init(type: Int = 0) {
|
||
self.type = type
|
||
}
|
||
|
||
/// 宽松解码统计块。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
type = try container.decodeLossyInt(forKey: .type) ?? 0
|
||
}
|
||
}
|
||
|
||
/// 排队列表分页块。
|
||
struct ScenicQueueHomeListBlock: Decodable, Equatable {
|
||
let list: [ScenicQueueTicket]
|
||
let total: Int
|
||
let page: Int
|
||
let pageSize: Int
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case list
|
||
case total
|
||
case page
|
||
case pageSize = "page_size"
|
||
}
|
||
|
||
/// 创建分页块。
|
||
init(list: [ScenicQueueTicket] = [], total: Int? = nil, page: Int = 1, pageSize: Int = 20) {
|
||
self.list = list
|
||
self.total = total ?? list.count
|
||
self.page = page
|
||
self.pageSize = pageSize
|
||
}
|
||
|
||
/// 宽松解码分页块。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
list = try container.decodeIfPresent([ScenicQueueTicket].self, forKey: .list) ?? []
|
||
total = try container.decodeLossyInt(forKey: .total) ?? list.count
|
||
page = try container.decodeLossyInt(forKey: .page) ?? 1
|
||
pageSize = try container.decodeLossyInt(forKey: .pageSize) ?? max(list.count, 20)
|
||
}
|
||
}
|
||
|
||
/// 单个排队取号记录。
|
||
struct ScenicQueueTicket: Decodable, Equatable, Identifiable {
|
||
let id: Int64
|
||
let queueCode: String
|
||
let mobile: String
|
||
let status: Int
|
||
let statusText: String
|
||
let waitMin: Int
|
||
let aheadCount: Int
|
||
let isCalled: Int
|
||
let createdAt: String
|
||
let calledAt: String
|
||
let expiredAt: String
|
||
let finishedAt: String
|
||
let queueBanLabel: String
|
||
let identityTag: String
|
||
let queueTime: String
|
||
let queueCountToday: Int
|
||
let uid: Int64
|
||
let markAsPhotog: Int
|
||
let markAsFreelancePhotog: Int
|
||
let isMissRequeue: Int
|
||
let missRequeueText: String
|
||
|
||
var phoneMasked: String {
|
||
let digits = mobile.filter(\.isNumber)
|
||
if mobile.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return "--" }
|
||
guard digits.count >= 7 else { return mobile }
|
||
return "\(digits.prefix(3))****\(digits.suffix(4))"
|
||
}
|
||
|
||
var dialPhoneDigits: String {
|
||
let digits = mobile.filter(\.isNumber)
|
||
return digits.isEmpty ? mobile.filter { !$0.isWhitespace } : digits
|
||
}
|
||
|
||
var queueTimeDisplay: String {
|
||
Self.formatQueueTime(queueTime.nonEmptyOrDefault(createdAt))
|
||
}
|
||
|
||
var skippedTimeDisplay: String {
|
||
Self.formatQueueTime(expiredAt.nonEmptyOrDefault(calledAt.nonEmptyOrDefault(createdAt)))
|
||
}
|
||
|
||
var shouldShowIdentityTag: Bool {
|
||
let text = identityTag.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
return !text.isEmpty && text != "普通用户"
|
||
}
|
||
|
||
var isMissRequeueRecord: Bool {
|
||
isMissRequeue == 1
|
||
}
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case queueCode = "queue_code"
|
||
case mobile
|
||
case status
|
||
case statusText = "status_text"
|
||
case waitMin = "wait_min"
|
||
case aheadCount = "ahead_count"
|
||
case isCalled = "is_called"
|
||
case createdAt = "created_at"
|
||
case calledAt = "called_at"
|
||
case expiredAt = "expired_at"
|
||
case finishedAt = "finished_at"
|
||
case queueBanLabel = "queue_ban_label"
|
||
case identityTag = "identity_tag"
|
||
case queueTime = "queue_time"
|
||
case queueCountToday = "queue_count_today"
|
||
case uid
|
||
case markAsPhotog = "mark_as_photog"
|
||
case markAsFreelancePhotog = "mark_as_freelance_photog"
|
||
case isMissRequeue = "is_miss_requeue"
|
||
case missRequeueText = "is_miss_requeue_text"
|
||
}
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum AlternateCodingKeys: String, CodingKey {
|
||
case queueNo = "queue_no"
|
||
case queueNumber = "queue_number"
|
||
case code
|
||
case phone
|
||
case statusName = "status_name"
|
||
}
|
||
|
||
/// 创建取号记录,主要用于本地状态替换和测试。
|
||
init(
|
||
id: Int64,
|
||
queueCode: String,
|
||
mobile: String,
|
||
status: Int,
|
||
statusText: String,
|
||
waitMin: Int,
|
||
aheadCount: Int,
|
||
isCalled: Int,
|
||
createdAt: String,
|
||
calledAt: String,
|
||
expiredAt: String,
|
||
finishedAt: String,
|
||
queueBanLabel: String = "",
|
||
identityTag: String = "",
|
||
queueTime: String = "",
|
||
queueCountToday: Int = 0,
|
||
uid: Int64 = 0,
|
||
markAsPhotog: Int = 0,
|
||
markAsFreelancePhotog: Int = 0,
|
||
isMissRequeue: Int = 0,
|
||
missRequeueText: String = ""
|
||
) {
|
||
self.id = id
|
||
self.queueCode = queueCode
|
||
self.mobile = mobile
|
||
self.status = status
|
||
self.statusText = statusText
|
||
self.waitMin = waitMin
|
||
self.aheadCount = aheadCount
|
||
self.isCalled = isCalled
|
||
self.createdAt = createdAt
|
||
self.calledAt = calledAt
|
||
self.expiredAt = expiredAt
|
||
self.finishedAt = finishedAt
|
||
self.queueBanLabel = queueBanLabel
|
||
self.identityTag = identityTag
|
||
self.queueTime = queueTime
|
||
self.queueCountToday = queueCountToday
|
||
self.uid = uid
|
||
self.markAsPhotog = markAsPhotog
|
||
self.markAsFreelancePhotog = markAsFreelancePhotog
|
||
self.isMissRequeue = isMissRequeue
|
||
self.missRequeueText = missRequeueText
|
||
}
|
||
|
||
/// 宽松解码取号记录,兼容 Android 历史字段别名。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
let alternate = try decoder.container(keyedBy: AlternateCodingKeys.self)
|
||
id = Int64(try container.decodeLossyInt(forKey: .id) ?? 0)
|
||
queueCode = try container.decodeLossyString(forKey: .queueCode)
|
||
.nonEmptyOrDefault(
|
||
try alternate.decodeLossyString(forKey: .queueNo)
|
||
.nonEmptyOrDefault(
|
||
try alternate.decodeLossyString(forKey: .queueNumber)
|
||
.nonEmptyOrDefault(try alternate.decodeLossyString(forKey: .code))
|
||
)
|
||
)
|
||
mobile = try container.decodeLossyString(forKey: .mobile)
|
||
.nonEmptyOrDefault(try alternate.decodeLossyString(forKey: .phone))
|
||
status = try container.decodeLossyInt(forKey: .status) ?? 0
|
||
statusText = try container.decodeLossyString(forKey: .statusText)
|
||
.nonEmptyOrDefault(try alternate.decodeLossyString(forKey: .statusName))
|
||
waitMin = try container.decodeLossyInt(forKey: .waitMin) ?? 0
|
||
aheadCount = try container.decodeLossyInt(forKey: .aheadCount) ?? 0
|
||
isCalled = try container.decodeLossyInt(forKey: .isCalled) ?? 0
|
||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||
calledAt = try container.decodeLossyString(forKey: .calledAt)
|
||
expiredAt = try container.decodeLossyString(forKey: .expiredAt)
|
||
finishedAt = try container.decodeLossyString(forKey: .finishedAt)
|
||
queueBanLabel = try container.decodeLossyString(forKey: .queueBanLabel).trimmingCharacters(in: .whitespacesAndNewlines)
|
||
identityTag = try container.decodeLossyString(forKey: .identityTag).trimmingCharacters(in: .whitespacesAndNewlines)
|
||
queueTime = try container.decodeLossyString(forKey: .queueTime)
|
||
queueCountToday = max(try container.decodeLossyInt(forKey: .queueCountToday) ?? 0, 0)
|
||
uid = Int64(try container.decodeLossyInt(forKey: .uid) ?? 0)
|
||
markAsPhotog = try container.decodeLossyInt(forKey: .markAsPhotog) ?? 0
|
||
markAsFreelancePhotog = try container.decodeLossyInt(forKey: .markAsFreelancePhotog) ?? 0
|
||
isMissRequeue = try container.decodeLossyInt(forKey: .isMissRequeue) ?? 0
|
||
missRequeueText = try container.decodeLossyString(forKey: .missRequeueText).trimmingCharacters(in: .whitespacesAndNewlines)
|
||
}
|
||
|
||
/// 格式化排队时间。
|
||
private static func formatQueueTime(_ raw: String) -> String {
|
||
let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !text.isEmpty else { return "--" }
|
||
if text.range(of: #"^\d{2}-\d{2}\s+\d{2}:\d{2}$"#, options: .regularExpression) != nil {
|
||
return text
|
||
}
|
||
let formatter = DateFormatter()
|
||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||
for format in ["yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm"] {
|
||
formatter.dateFormat = format
|
||
if let date = formatter.date(from: text) {
|
||
formatter.dateFormat = "MM-dd HH:mm"
|
||
return formatter.string(from: date)
|
||
}
|
||
}
|
||
return text
|
||
}
|
||
}
|
||
|
||
/// 排队动作请求体。
|
||
struct ScenicQueueActionRequest: Encodable {
|
||
let id: Int64
|
||
}
|
||
|
||
/// socket token 响应。
|
||
struct SocketTokenResponse: Decodable, Equatable {
|
||
let socketToken: String
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case socketToken = "socket_token"
|
||
}
|
||
|
||
/// 创建 socket token 响应。
|
||
init(socketToken: String = "") {
|
||
self.socketToken = socketToken
|
||
}
|
||
|
||
/// 宽松解码 socket token。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
socketToken = try container.decodeLossyString(forKey: .socketToken)
|
||
}
|
||
}
|
||
|
||
/// 排队动作响应。
|
||
struct ScenicQueueActionData: Decodable, Equatable {
|
||
let id: Int64
|
||
let status: Int
|
||
let statusText: String
|
||
let calledAt: String?
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case status
|
||
case statusText = "status_text"
|
||
case calledAt = "called_at"
|
||
}
|
||
|
||
/// 创建动作响应。
|
||
init(id: Int64 = 0, status: Int = 0, statusText: String = "", calledAt: String? = nil) {
|
||
self.id = id
|
||
self.status = status
|
||
self.statusText = statusText
|
||
self.calledAt = calledAt
|
||
}
|
||
|
||
/// 宽松解码动作响应。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
id = Int64(try container.decodeLossyInt(forKey: .id) ?? 0)
|
||
status = try container.decodeLossyInt(forKey: .status) ?? 0
|
||
statusText = try container.decodeLossyString(forKey: .statusText)
|
||
calledAt = try container.decodeIfPresent(String.self, forKey: .calledAt)
|
||
}
|
||
}
|
||
|
||
typealias ScenicQueueCallData = ScenicQueueActionData
|
||
typealias ScenicQueuePassData = ScenicQueueActionData
|
||
typealias ScenicQueueFinishData = ScenicQueueActionData
|
||
typealias QueueItem = ScenicQueueTicket
|
||
|
||
/// 重新排队请求体。
|
||
struct ScenicQueueRequeueInsertBeforeRequest: Encodable {
|
||
let recordId: Int64
|
||
let operatorId: Int
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case recordId = "record_id"
|
||
case operatorId = "operator_id"
|
||
}
|
||
}
|
||
|
||
/// 用户标记请求体。
|
||
struct ScenicQueueUserMarkRequest: Encodable, Equatable {
|
||
let uid: Int64
|
||
let scenicId: Int64
|
||
let markAsFreelancePhotog: Int
|
||
let queueBanDays: Int?
|
||
let operatorId: Int?
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
enum CodingKeys: String, CodingKey {
|
||
case uid
|
||
case scenicId = "scenic_id"
|
||
case markAsFreelancePhotog = "mark_as_freelance_photog"
|
||
case queueBanDays = "queue_ban_days"
|
||
case operatorId = "operator_id"
|
||
}
|
||
}
|
||
|
||
/// 排队列表类型。
|
||
enum QueueListType: Int, CaseIterable, Identifiable {
|
||
case queueing = 1
|
||
case passed = 2
|
||
|
||
var id: Int { rawValue }
|
||
|
||
/// 分段标题。
|
||
var title: String {
|
||
switch self {
|
||
case .queueing:
|
||
return "当前排队"
|
||
case .passed:
|
||
return "过号列表"
|
||
}
|
||
}
|
||
|
||
/// 空态文案。
|
||
var emptyText: String {
|
||
switch self {
|
||
case .queueing:
|
||
return "暂无排队"
|
||
case .passed:
|
||
return "暂无过号"
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 远端叫号提示。
|
||
struct ScenicQueueRemoteCallAnnouncement: Identifiable, Equatable {
|
||
let id: Int64
|
||
let queueCode: String
|
||
}
|
||
|
||
private extension KeyedDecodingContainer {
|
||
/// 解码宽松字符串。
|
||
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 ""
|
||
}
|
||
|
||
/// 解码宽松整数。
|
||
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),
|
||
case let text = value.trimmingCharacters(in: .whitespacesAndNewlines),
|
||
!text.isEmpty {
|
||
return Int(text) ?? Int(Double(text) ?? 0)
|
||
}
|
||
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
|
||
return value ? 1 : 0
|
||
}
|
||
return nil
|
||
}
|
||
|
||
/// 解码宽松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),
|
||
case let text = value.trimmingCharacters(in: .whitespacesAndNewlines),
|
||
!text.isEmpty {
|
||
return Double(text)
|
||
}
|
||
return nil
|
||
}
|
||
}
|
||
|
||
private extension String {
|
||
/// non空或默认相关逻辑。
|
||
func nonEmptyOrDefault(_ fallback: String) -> String {
|
||
let text = trimmingCharacters(in: .whitespacesAndNewlines)
|
||
return text.isEmpty ? fallback : text
|
||
}
|
||
}
|