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>
This commit is contained in:
@ -10,17 +10,29 @@ import Foundation
|
||||
/// 排队管理服务协议,定义列表、动作、设置、二维码和实时 token 能力。
|
||||
@MainActor
|
||||
protocol ScenicQueueServing: AnyObject {
|
||||
/// scenic排队统计相关逻辑。
|
||||
func scenicQueueStats(scenicId: Int, scenicSpotId: Int) async throws -> ScenicQueueStatsData
|
||||
/// scenic排队Home相关逻辑。
|
||||
func scenicQueueHome(scenicId: Int, scenicSpotId: Int, type: Int, page: Int, pageSize: Int) async throws -> ScenicQueueHomeData
|
||||
/// socket令牌相关逻辑。
|
||||
func socketToken() async throws -> SocketTokenResponse
|
||||
/// scenic排队Call相关逻辑。
|
||||
func scenicQueueCall(id: Int64) async throws -> ScenicQueueCallData
|
||||
/// scenic排队Pass相关逻辑。
|
||||
func scenicQueuePass(id: Int64) async throws -> ScenicQueuePassData
|
||||
/// scenic排队Finish相关逻辑。
|
||||
func scenicQueueFinish(id: Int64) async throws -> ScenicQueueFinishData
|
||||
/// scenic排队RequeueInsertBefore相关逻辑。
|
||||
func scenicQueueRequeueInsertBefore(recordId: Int64, operatorId: Int) async throws
|
||||
/// scenic排队UserMark相关逻辑。
|
||||
func scenicQueueUserMark(_ request: ScenicQueueUserMarkRequest) async throws
|
||||
/// scenic排队Setting相关逻辑。
|
||||
func scenicQueueSetting(scenicId: Int, scenicSpotId: Int?) async throws -> ScenicQueueSettingData
|
||||
/// scenic排队保存Setting相关逻辑。
|
||||
func scenicQueueSaveSetting(_ request: ScenicQueueSaveSettingRequest) async throws
|
||||
/// scenic排队Shoot排队QRCode相关逻辑。
|
||||
func scenicQueueShootQueueQRCode(scenicId: Int, scenicSpotId: Int) async throws -> ScenicQueueShootQueueQRCodeData
|
||||
/// scenic排队SettingChangeLog相关逻辑。
|
||||
func scenicQueueSettingChangeLog(scenicId: Int, scenicSpotId: Int?, page: Int, pageSize: Int) async throws -> ScenicQueueSettingChangeLogData
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ struct ScenicQueueStatsData: Decodable, Equatable {
|
||||
let avgWaitMin: Double
|
||||
let time: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case queueCount = "queue_count"
|
||||
case avgWaitMin = "avg_wait_min"
|
||||
@ -41,6 +42,7 @@ struct ScenicQueueHomeData: Decodable, Equatable {
|
||||
let list: ScenicQueueHomeListBlock?
|
||||
let time: String?
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case stats
|
||||
case list
|
||||
@ -73,6 +75,7 @@ struct ScenicQueueHomeData: Decodable, Equatable {
|
||||
struct ScenicQueueHomeStats: Decodable, Equatable {
|
||||
let type: Int
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
}
|
||||
@ -96,6 +99,7 @@ struct ScenicQueueHomeListBlock: Decodable, Equatable {
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case list
|
||||
case total
|
||||
@ -174,6 +178,7 @@ struct ScenicQueueTicket: Decodable, Equatable, Identifiable {
|
||||
isMissRequeue == 1
|
||||
}
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case queueCode = "queue_code"
|
||||
@ -198,6 +203,7 @@ struct ScenicQueueTicket: Decodable, Equatable, Identifiable {
|
||||
case missRequeueText = "is_miss_requeue_text"
|
||||
}
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum AlternateCodingKeys: String, CodingKey {
|
||||
case queueNo = "queue_no"
|
||||
case queueNumber = "queue_number"
|
||||
@ -289,6 +295,7 @@ struct ScenicQueueTicket: Decodable, Equatable, Identifiable {
|
||||
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 "--" }
|
||||
@ -317,6 +324,7 @@ struct ScenicQueueActionRequest: Encodable {
|
||||
struct SocketTokenResponse: Decodable, Equatable {
|
||||
let socketToken: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case socketToken = "socket_token"
|
||||
}
|
||||
@ -340,6 +348,7 @@ struct ScenicQueueActionData: Decodable, Equatable {
|
||||
let statusText: String
|
||||
let calledAt: String?
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case status
|
||||
@ -375,6 +384,7 @@ struct ScenicQueueRequeueInsertBeforeRequest: Encodable {
|
||||
let recordId: Int64
|
||||
let operatorId: Int
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case recordId = "record_id"
|
||||
case operatorId = "operator_id"
|
||||
@ -389,6 +399,7 @@ struct ScenicQueueUserMarkRequest: Encodable, Equatable {
|
||||
let queueBanDays: Int?
|
||||
let operatorId: Int?
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case uid
|
||||
case scenicId = "scenic_id"
|
||||
@ -433,6 +444,7 @@ struct ScenicQueueRemoteCallAnnouncement: Identifiable, Equatable {
|
||||
}
|
||||
|
||||
private extension KeyedDecodingContainer {
|
||||
/// 解码宽松字符串。
|
||||
func decodeLossyString(forKey key: Key) throws -> String {
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
return value
|
||||
@ -449,6 +461,7 @@ private extension KeyedDecodingContainer {
|
||||
return ""
|
||||
}
|
||||
|
||||
/// 解码宽松整数。
|
||||
func decodeLossyInt(forKey key: Key) throws -> Int? {
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||||
return value
|
||||
@ -467,6 +480,7 @@ private extension KeyedDecodingContainer {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 解码宽松Double。
|
||||
func decodeLossyDouble(forKey key: Key) throws -> Double? {
|
||||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||||
return value
|
||||
@ -484,6 +498,7 @@ private extension KeyedDecodingContainer {
|
||||
}
|
||||
|
||||
private extension String {
|
||||
/// non空或默认相关逻辑。
|
||||
func nonEmptyOrDefault(_ fallback: String) -> String {
|
||||
let text = trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return text.isEmpty ? fallback : text
|
||||
|
||||
@ -13,6 +13,7 @@ struct ScenicQueueSettingData: Decodable, Equatable {
|
||||
let setting: ScenicQueueSettingItem?
|
||||
let time: String?
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case exists
|
||||
case setting
|
||||
@ -64,6 +65,7 @@ struct ScenicQueueSettingItem: Decodable, Equatable {
|
||||
let createdAt: String
|
||||
let updatedAt: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case scenicId = "scenic_id"
|
||||
@ -131,6 +133,7 @@ struct ScenicQueueVoiceBroadcastItem: Codable, Identifiable, Equatable {
|
||||
let content: String
|
||||
let sortOrder: Int
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case content
|
||||
case sortOrder = "sort_order"
|
||||
@ -175,6 +178,7 @@ struct ScenicQueueSaveSettingRequest: Encodable, Equatable {
|
||||
let status: Int
|
||||
let remark: String?
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicId = "scenic_id"
|
||||
case scenicSpotId = "scenic_spot_id"
|
||||
@ -208,6 +212,7 @@ struct ScenicQueueSettingChangeLogData: Decodable, Equatable {
|
||||
let pageSize: Int
|
||||
let list: [ScenicQueueSettingChangeLogItem]
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case total
|
||||
case page
|
||||
@ -243,6 +248,7 @@ struct ScenicQueueSettingChangeLogItem: Decodable, Equatable, Identifiable {
|
||||
let displayText: String
|
||||
let createdAt: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case scenicSpotName = "scenic_spot_name"
|
||||
@ -270,6 +276,7 @@ struct ScenicQueueSettingChangeLogItem: Decodable, Equatable, Identifiable {
|
||||
struct ScenicQueueShootQueueQRCodeData: Decodable, Equatable {
|
||||
let qrcodeUrl: String
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case qrcodeUrl = "qrcode_url"
|
||||
}
|
||||
@ -309,6 +316,7 @@ struct ScenicQueueSettingsSnapshot: Codable, Equatable {
|
||||
var businessStartTime: String = "10:00"
|
||||
var businessEndTime: String = "20:00"
|
||||
|
||||
/// 枚举,定义相关常量或状态。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case shootMinute = "shoot_minute"
|
||||
case shootSecond = "shoot_second"
|
||||
@ -334,6 +342,7 @@ struct ScenicQueueSettingsSnapshot: Codable, Equatable {
|
||||
}
|
||||
|
||||
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) }
|
||||
@ -342,6 +351,7 @@ private extension KeyedDecodingContainer {
|
||||
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) }
|
||||
@ -354,6 +364,7 @@ private extension KeyedDecodingContainer {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 解码宽松布尔。
|
||||
func decodeLossyBool(forKey key: Key) throws -> Bool? {
|
||||
if let value = try? decodeIfPresent(Bool.self, forKey: key) { return value }
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) { return value != 0 }
|
||||
|
||||
@ -155,17 +155,20 @@ enum ScenicQueueSettingsStore {
|
||||
UserDefaults.standard.set(data, forKey: "scenic_\(scenicId)_spot_\(spotId)_\(ScenicQueueLocalSettings.presetVoicesKey)")
|
||||
}
|
||||
|
||||
/// 规范化dUserId格式。
|
||||
private static func normalizedUserId(_ userId: String?) -> String {
|
||||
let text = userId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return text.isEmpty ? "anonymous" : text
|
||||
}
|
||||
|
||||
/// positive整数相关逻辑。
|
||||
private static func positiveInt(forKey key: String) -> Int? {
|
||||
guard UserDefaults.standard.object(forKey: key) != nil else { return nil }
|
||||
let value = UserDefaults.standard.integer(forKey: key)
|
||||
return value > 0 ? value : nil
|
||||
}
|
||||
|
||||
/// legacy正数整数相关逻辑。
|
||||
private static func legacyPositiveInt(forKey key: String) -> Int? {
|
||||
positiveInt(forKey: key)
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import UIKit
|
||||
final class QueueManagementViewController: ModuleTableViewController {
|
||||
private let viewModel = QueueManagementViewModel()
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
title = "排队管理"
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
@ -23,13 +24,16 @@ final class QueueManagementViewController: ModuleTableViewController {
|
||||
wireViewModel(viewModel) { [weak self] in self?.updateTitle() }
|
||||
}
|
||||
|
||||
/// table行Count相关逻辑。
|
||||
override func tableRowCount() -> Int { viewModel.items.count }
|
||||
|
||||
/// 配置Cell展示内容。
|
||||
override func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) {
|
||||
let item = viewModel.items[indexPath.row]
|
||||
cell.configure(title: item.phoneMasked, subtitle: item.statusText, detail: item.queueCode)
|
||||
}
|
||||
|
||||
/// 刷新Content。
|
||||
override func reloadContent() async {
|
||||
await viewModel.reload(
|
||||
api: services.scenicQueueAPI,
|
||||
@ -40,10 +44,12 @@ final class QueueManagementViewController: ModuleTableViewController {
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
/// 更新Title状态。
|
||||
private func updateTitle() {
|
||||
title = "排队 \(viewModel.queueCount)"
|
||||
}
|
||||
|
||||
/// open设置相关逻辑。
|
||||
@objc private func openSettings() {
|
||||
navigationController?.pushViewController(ScenicQueueSettingsViewController(), animated: true)
|
||||
}
|
||||
@ -55,14 +61,17 @@ extension QueueManagementViewModel: ViewModelBindable {}
|
||||
final class ScenicQueueSettingsViewController: ModuleTableViewController {
|
||||
private let viewModel = ScenicQueueSettingsViewModel()
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
title = "排队设置"
|
||||
super.viewDidLoad()
|
||||
wireViewModel(viewModel) { }
|
||||
}
|
||||
|
||||
/// table行Count相关逻辑。
|
||||
override func tableRowCount() -> Int { viewModel.scenicSpots.count }
|
||||
|
||||
/// 配置Cell展示内容。
|
||||
override func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) {
|
||||
let spot = viewModel.scenicSpots[indexPath.row]
|
||||
let selected = viewModel.selectedSpotId == spot.id
|
||||
@ -70,10 +79,12 @@ final class ScenicQueueSettingsViewController: ModuleTableViewController {
|
||||
cell.accessoryType = selected ? .checkmark : .none
|
||||
}
|
||||
|
||||
/// didSelectTableRow 回调处理。
|
||||
override func didSelectTableRow(at indexPath: IndexPath) {
|
||||
viewModel.selectedSpotId = viewModel.scenicSpots[indexPath.row].id
|
||||
}
|
||||
|
||||
/// 刷新Content。
|
||||
override func reloadContent() async {
|
||||
await viewModel.load(
|
||||
api: services.scenicQueueAPI,
|
||||
|
||||
@ -209,6 +209,7 @@ final class QueueManagementViewModel {
|
||||
return max(snapshot.shootMinute * 60 + snapshot.shootSecond, 0)
|
||||
}
|
||||
|
||||
/// 处理SocketMessage相关事件。
|
||||
private func handleSocketMessage(_ message: ScenicQueueSocketMessage, api: any ScenicQueueServing, scenicId: Int, userId: String?) async {
|
||||
guard message.isScenicQueueEvent,
|
||||
let params = message.data?.params,
|
||||
@ -227,6 +228,7 @@ final class QueueManagementViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理RemoteTicketCalled相关事件。
|
||||
private func handleRemoteTicketCalled(params: ScenicQueueSocketParams, api: any ScenicQueueServing, scenicId: Int, userId: String?) async {
|
||||
guard let recordId = params.recordId, recordId > 0 else { return }
|
||||
if let operatorUid = params.operatorUid,
|
||||
@ -251,6 +253,7 @@ final class QueueManagementViewModel {
|
||||
await reloadCurrentSpot(api: api, scenicId: scenicId, userId: userId)
|
||||
}
|
||||
|
||||
/// 加载QueueingTickets数据。
|
||||
private func loadQueueingTickets(api: any ScenicQueueServing, scenicId: Int, scenicSpotId: Int) async throws -> [QueueItem] {
|
||||
let home = try await api.scenicQueueHome(
|
||||
scenicId: scenicId,
|
||||
@ -262,6 +265,7 @@ final class QueueManagementViewModel {
|
||||
return home.list?.list ?? []
|
||||
}
|
||||
|
||||
/// consume待处理远程CalledTickets相关逻辑。
|
||||
private func consumePendingRemoteCalledTickets(from tickets: [QueueItem]) {
|
||||
guard let ticket = tickets.first(where: {
|
||||
pendingRemoteCalledRecordIds.contains($0.id) && ($0.isCalled == 1 || $0.status == 1 || $0.statusText.contains("已叫号"))
|
||||
@ -272,6 +276,7 @@ final class QueueManagementViewModel {
|
||||
remoteCallAnnouncement = ScenicQueueRemoteCallAnnouncement(id: ticket.id, queueCode: ticket.queueCode)
|
||||
}
|
||||
|
||||
/// 加载Page数据。
|
||||
private func loadPage(api: any ScenicQueueServing, scenicId: Int, userId: String?, page targetPage: Int) async throws {
|
||||
guard let scenicSpotId = selectedSpotId else { return }
|
||||
let home = try await api.scenicQueueHome(
|
||||
@ -300,6 +305,7 @@ final class QueueManagementViewModel {
|
||||
refreshShootingCallConfig(userId: userId, scenicId: scenicId, spotId: scenicSpotId)
|
||||
}
|
||||
|
||||
/// mark排队Called相关逻辑。
|
||||
private func markQueueCalled(id: Int64, statusText: String) {
|
||||
let trimmedStatus = statusText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
items = items.map { item in
|
||||
@ -330,6 +336,7 @@ final class QueueManagementViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 重置状态。
|
||||
private func reset() {
|
||||
stopRealtime()
|
||||
loading = false
|
||||
@ -343,6 +350,7 @@ final class QueueManagementViewModel {
|
||||
clearQueueData()
|
||||
}
|
||||
|
||||
/// 清空排队数据。
|
||||
private func clearQueueData() {
|
||||
loadingMore = false
|
||||
items = []
|
||||
@ -354,6 +362,7 @@ final class QueueManagementViewModel {
|
||||
lastSyncTimeText = "--"
|
||||
}
|
||||
|
||||
/// 刷新ShootingCallConfig展示。
|
||||
private func refreshShootingCallConfig(userId: String?, scenicId: Int, spotId: Int) {
|
||||
let snapshot = ScenicQueueSettingsStore.settingsSnapshot(userId: userId, scenicId: scenicId, spotId: spotId) ?? ScenicQueueSettingsSnapshot()
|
||||
showStartShootingButton = snapshot.showStartShootingButton
|
||||
@ -362,6 +371,7 @@ final class QueueManagementViewModel {
|
||||
prepareCallButtonEnabled = snapshot.prepareCallButtonEnabled
|
||||
}
|
||||
|
||||
/// 同步QueueSettingFromServerIfMatchesLocal状态。
|
||||
private func syncQueueSettingFromServerIfMatchesLocal(api: any ScenicQueueServing, userId: String?, scenicId: Int, spotId: Int) async {
|
||||
guard let data = try? await api.scenicQueueSetting(scenicId: scenicId, scenicSpotId: spotId),
|
||||
data.exists,
|
||||
@ -648,6 +658,7 @@ final class ScenicQueueSettingsViewModel {
|
||||
presetVoices.remove(at: index)
|
||||
}
|
||||
|
||||
/// 重置SettingsState状态。
|
||||
private func resetSettingsState() {
|
||||
loading = false
|
||||
scenicSpots = []
|
||||
@ -678,6 +689,7 @@ final class ScenicQueueSettingsViewModel {
|
||||
qrcodeURL = ""
|
||||
}
|
||||
|
||||
/// apply 业务逻辑。
|
||||
private func apply(_ setting: ScenicQueueSettingItem?) {
|
||||
guard let setting else { return }
|
||||
if let spotId = setting.scenicSpotId { selectedSpotId = spotId }
|
||||
@ -707,6 +719,7 @@ final class ScenicQueueSettingsViewModel {
|
||||
if !voices.isEmpty { presetVoices = Array(voices.prefix(5)) }
|
||||
}
|
||||
|
||||
/// apply 业务逻辑。
|
||||
private func apply(_ snapshot: ScenicQueueSettingsSnapshot) {
|
||||
photoEstimateMin = "\(min(max(snapshot.shootMinute, 0), 30))"
|
||||
photoEstimateSec = "\(min(max(snapshot.shootSecond, 0), 59))"
|
||||
@ -730,6 +743,7 @@ final class ScenicQueueSettingsViewModel {
|
||||
businessEndTime = Self.parseBusinessTime(snapshot.businessEndTime, fallback: Self.businessTime(hour: 20, minute: 0))
|
||||
}
|
||||
|
||||
/// 规范化dMaxQueueRangeForDisplay格式。
|
||||
private func normalizedMaxQueueRangeForDisplay() throws -> String {
|
||||
let normalized = AppFormValidator.normalizedMoneyForSubmit(maxQueueRangeKm)
|
||||
guard !normalized.isEmpty,
|
||||
@ -745,12 +759,14 @@ final class ScenicQueueSettingsViewModel {
|
||||
return String(format: "%.2f", NSDecimalNumber(decimal: value).doubleValue)
|
||||
}
|
||||
|
||||
/// queueDistanceMeter用于提交相关逻辑。
|
||||
private func queueDistanceMeterForSubmit() throws -> Int {
|
||||
_ = try normalizedMaxQueueRangeForDisplay()
|
||||
let value = Decimal(string: maxQueueRangeKm, locale: Locale(identifier: "en_US_POSIX")) ?? 0
|
||||
return NSDecimalNumber(decimal: value * Decimal(1000)).intValue
|
||||
}
|
||||
|
||||
/// 加载QRCodeImage数据。
|
||||
private func loadQRCodeImage(from text: String) async throws -> UIImage {
|
||||
if let url = URL(string: text), ["http", "https"].contains(url.scheme?.lowercased()) {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
@ -760,6 +776,7 @@ final class ScenicQueueSettingsViewModel {
|
||||
throw APIError.networkFailed("二维码图片生成失败")
|
||||
}
|
||||
|
||||
/// generateQRCode相关逻辑。
|
||||
private static func generateQRCode(from string: String) -> UIImage? {
|
||||
let filter = CIFilter.qrCodeGenerator()
|
||||
filter.message = Data(string.utf8)
|
||||
@ -770,12 +787,14 @@ final class ScenicQueueSettingsViewModel {
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
|
||||
/// saveTiming快照相关逻辑。
|
||||
private func saveTimingSnapshot(min: Int, sec: Int, interval: Int, countdown: Int) {
|
||||
UserDefaults.standard.set(max(min * 60 + sec, 0), forKey: ScenicQueueLocalSettings.photoEstimateSecondsKey)
|
||||
UserDefaults.standard.set(interval, forKey: ScenicQueueLocalSettings.broadcastIntervalSecondsKey)
|
||||
UserDefaults.standard.set(countdown, forKey: ScenicQueueLocalSettings.countdownThresholdSecondsKey)
|
||||
}
|
||||
|
||||
/// saveShootingCall设置相关逻辑。
|
||||
private func saveShootingCallSettings(autoCallAhead: Int) {
|
||||
UserDefaults.standard.set(showStartShootingButton, forKey: ScenicQueueLocalSettings.showStartShootingButtonKey)
|
||||
UserDefaults.standard.set(autoCallAhead, forKey: ScenicQueueLocalSettings.autoCallAheadCountKey)
|
||||
@ -783,10 +802,12 @@ final class ScenicQueueSettingsViewModel {
|
||||
UserDefaults.standard.set(prepareCallButtonEnabled, forKey: ScenicQueueLocalSettings.prepareCallButtonEnabledKey)
|
||||
}
|
||||
|
||||
/// save自定义文本Locally相关逻辑。
|
||||
private func saveCustomTextLocally() {
|
||||
ScenicQueueSettingsStore.saveCustomTtsText(customTtsText, userId: currentUserId, scenicId: currentScenicId, spotId: selectedSpotId)
|
||||
}
|
||||
|
||||
/// voiceBroadcasts用于保存载荷相关逻辑。
|
||||
private func voiceBroadcastsForSavePayload() -> [ScenicQueueVoiceBroadcastItem] {
|
||||
Array(presetVoices
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
@ -796,6 +817,7 @@ final class ScenicQueueSettingsViewModel {
|
||||
.map { index, content in ScenicQueueVoiceBroadcastItem(content: content, sortOrder: index + 1) })
|
||||
}
|
||||
|
||||
/// 创建Snapshot实例。
|
||||
private func makeSnapshot(
|
||||
min: Int? = nil,
|
||||
sec: Int? = nil,
|
||||
@ -832,18 +854,22 @@ final class ScenicQueueSettingsViewModel {
|
||||
)
|
||||
}
|
||||
|
||||
/// coerced播报间隔相关逻辑。
|
||||
static func coercedBroadcastInterval(_ raw: Int) -> Int {
|
||||
(40...60).contains(raw) ? raw : 50
|
||||
}
|
||||
|
||||
/// coerced倒计时阈值相关逻辑。
|
||||
static func coercedCountdownThreshold(_ raw: Int) -> Int {
|
||||
(10...30).contains(raw) ? raw : 15
|
||||
}
|
||||
|
||||
/// business时间相关逻辑。
|
||||
static func businessTime(hour: Int, minute: Int) -> Date {
|
||||
Calendar.current.date(from: DateComponents(hour: hour, minute: minute)) ?? Date()
|
||||
}
|
||||
|
||||
/// 解析BusinessTime数据。
|
||||
static func parseBusinessTime(_ raw: String, fallback: Date) -> Date {
|
||||
let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !text.isEmpty else { return fallback }
|
||||
@ -856,6 +882,7 @@ final class ScenicQueueSettingsViewModel {
|
||||
return fallback
|
||||
}
|
||||
|
||||
/// business时间载荷相关逻辑。
|
||||
static func businessTimePayload(_ date: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
@ -863,6 +890,7 @@ final class ScenicQueueSettingsViewModel {
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
/// queueDistanceDisplay相关逻辑。
|
||||
private static func queueDistanceDisplay(meters: Int) -> String {
|
||||
let clamped = min(max(meters, 0), 9_999_990)
|
||||
return String(format: "%.2f", Double(clamped) / 1000)
|
||||
@ -875,6 +903,7 @@ private extension String {
|
||||
return text.isEmpty ? nil : text
|
||||
}
|
||||
|
||||
/// non空或默认相关逻辑。
|
||||
func nonEmptyOrDefault(_ fallback: String) -> String {
|
||||
let text = trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return text.isEmpty ? fallback : text
|
||||
|
||||
Reference in New Issue
Block a user