从 iOS 17 Observation 迁移至 iOS 16 兼容的 Combine 架构

将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 10:16:35 +08:00
parent 0314033a7f
commit 703078352c
127 changed files with 2320 additions and 1465 deletions

View File

@ -6,44 +6,43 @@
//
import Foundation
import Observation
import Combine
import Photos
import SwiftUI
import UIKit
@MainActor
@Observable
/// ViewModel
final class QueueManagementViewModel {
var loading = false
var loadingMore = false
var items: [QueueItem] = []
var scenicSpots: [ScenicSpotItem] = []
var selectedSpotId: Int?
var selectedListType: QueueListType = .queueing
var queueCount = 0
var avgWaitMin = 0.0
var totalCount = 0
var hasMore = false
var page = 1
var lastSyncTimeText = "--"
var queueGatePassed = false
var selectedSpotName = "--"
var showStartShootingButton = true
var autoCallAheadCount = 0
var quickCallButtonEnabled = false
var prepareCallButtonEnabled = false
var remoteCallAnnouncement: ScenicQueueRemoteCallAnnouncement?
var errorMessage: String?
final class QueueManagementViewModel: ObservableObject {
@Published var loading = false
@Published var loadingMore = false
@Published var items: [QueueItem] = []
@Published var scenicSpots: [ScenicSpotItem] = []
@Published var selectedSpotId: Int?
@Published var selectedListType: QueueListType = .queueing
@Published var queueCount = 0
@Published var avgWaitMin = 0.0
@Published var totalCount = 0
@Published var hasMore = false
@Published var page = 1
@Published var lastSyncTimeText = "--"
@Published var queueGatePassed = false
@Published var selectedSpotName = "--"
@Published var showStartShootingButton = true
@Published var autoCallAheadCount = 0
@Published var quickCallButtonEnabled = false
@Published var prepareCallButtonEnabled = false
@Published var remoteCallAnnouncement: ScenicQueueRemoteCallAnnouncement?
@Published var errorMessage: String?
@ObservationIgnored private let pageSize = 10
@ObservationIgnored private let socketClient = ScenicQueueSocketClient()
@ObservationIgnored private var realtimeSpotId: Int?
@ObservationIgnored private var realtimeScenicId: Int?
@ObservationIgnored private var currentUserId: String?
@ObservationIgnored private var currentScenicId: Int?
@ObservationIgnored private var pendingRemoteCalledRecordIds = Set<Int64>()
@ObservationIgnored private var handledRemoteCallEventIds = Set<String>()
private let pageSize = 10
private let socketClient = ScenicQueueSocketClient()
@Published private var realtimeSpotId: Int?
@Published private var realtimeScenicId: Int?
@Published private var currentUserId: String?
@Published private var currentScenicId: Int?
@Published private var pendingRemoteCalledRecordIds = Set<Int64>()
@Published private var handledRemoteCallEventIds = Set<String>()
///
var pendingCount: Int {
@ -417,41 +416,40 @@ final class QueueManagementViewModel {
}
@MainActor
@Observable
/// ViewModel
final class ScenicQueueSettingsViewModel {
var loading = false
var message: String?
var scenicSpots: [ScenicSpotItem] = []
var selectedSpotId: Int?
var photoEstimateMin = "0"
var photoEstimateSec = "30"
var firstAhead = "0"
var firstSms = false
var firstPhone = false
var secondAhead = "0"
var secondSms = false
var secondPhone = false
var broadcastIntervalSec = "50"
var countdownThresholdSec = "15"
var maxQueueRangeKm = "0.00"
var localQueueLimit = "0"
var localPassDelay = "1"
var showStartShootingButton = true
var autoCallAheadCount = "0"
var queueOpen = true
var businessStartTime = ScenicQueueSettingsViewModel.businessTime(hour: 10, minute: 0)
var businessEndTime = ScenicQueueSettingsViewModel.businessTime(hour: 20, minute: 0)
var customTtsText = ""
var presetVoices: [String] = []
var quickCallButtonEnabled = false
var prepareCallButtonEnabled = false
var logs: [ScenicQueueSettingChangeLogItem] = []
var qrcodeURL = ""
final class ScenicQueueSettingsViewModel: ObservableObject {
@Published var loading = false
@Published var message: String?
@Published var scenicSpots: [ScenicSpotItem] = []
@Published var selectedSpotId: Int?
@Published var photoEstimateMin = "0"
@Published var photoEstimateSec = "30"
@Published var firstAhead = "0"
@Published var firstSms = false
@Published var firstPhone = false
@Published var secondAhead = "0"
@Published var secondSms = false
@Published var secondPhone = false
@Published var broadcastIntervalSec = "50"
@Published var countdownThresholdSec = "15"
@Published var maxQueueRangeKm = "0.00"
@Published var localQueueLimit = "0"
@Published var localPassDelay = "1"
@Published var showStartShootingButton = true
@Published var autoCallAheadCount = "0"
@Published var queueOpen = true
@Published var businessStartTime = ScenicQueueSettingsViewModel.businessTime(hour: 10, minute: 0)
@Published var businessEndTime = ScenicQueueSettingsViewModel.businessTime(hour: 20, minute: 0)
@Published var customTtsText = ""
@Published var presetVoices: [String] = []
@Published var quickCallButtonEnabled = false
@Published var prepareCallButtonEnabled = false
@Published var logs: [ScenicQueueSettingChangeLogItem] = []
@Published var qrcodeURL = ""
@ObservationIgnored private let speaker = ScenicQueueSpeechService()
@ObservationIgnored private var currentUserId: String?
@ObservationIgnored private var currentScenicId: Int?
private let speaker = ScenicQueueSpeechService()
@Published private var currentUserId: String?
@Published private var currentScenicId: Int?
///
func load(api: any ScenicQueueServing, scenicId: Int?, userId: String?, spots: [ScenicSpotItem]) async {