Files
suixinkan_ios_uikit/suixinkan_ios/Features/QueueManagement/Models/ScenicQueueModels.swift
汉秋 d99a5b1bf8 Advance UIKit rewrite with AMap integration and core UI modules.
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>
2026-06-26 15:16:12 +08:00

507 lines
17 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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
}
}