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:
@ -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