从 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

@ -9,12 +9,12 @@ import SwiftUI
///
struct QueueManagementView: View {
@Environment(AccountContext.self) private var accountContext
@Environment(ScenicSpotContext.self) private var scenicSpotContext
@Environment(ScenicQueueAPI.self) private var queueAPI
@Environment(ScenicQueueRuntime.self) private var queueRuntime
@Environment(ToastCenter.self) private var toastCenter
@State private var viewModel = QueueManagementViewModel()
@EnvironmentObject private var accountContext: AccountContext
@EnvironmentObject private var scenicSpotContext: ScenicSpotContext
@Environment(\.scenicQueueAPI) private var queueAPI
@EnvironmentObject private var queueRuntime: ScenicQueueRuntime
@EnvironmentObject private var toastCenter: ToastCenter
@StateObject private var viewModel = QueueManagementViewModel()
@State private var pendingAction: QueueActionConfirmation?
@State private var markTarget: QueueItem?
@State private var processingId: Int64?
@ -27,7 +27,7 @@ struct QueueManagementView: View {
VStack(spacing: AppMetrics.Spacing.medium) {
header
if currentScenicId == nil {
ContentUnavailableView("请先选择景区", systemImage: "map", description: Text("排队管理需要当前景区上下文"))
AppContentUnavailableView("请先选择景区", systemImage: "map", description: Text("排队管理需要当前景区上下文"))
.frame(minHeight: 260)
} else if !viewModel.queueGatePassed {
gateCard
@ -79,12 +79,12 @@ struct QueueManagementView: View {
.refreshable {
await reload()
}
.onChange(of: viewModel.remoteCallAnnouncement) { _, announcement in
.onChange(of: viewModel.remoteCallAnnouncement) { announcement in
if let announcement {
toastCenter.show("远端已叫号 \(announcement.queueCode.isEmpty ? "当前游客" : announcement.queueCode)")
}
}
.onChange(of: viewModel.errorMessage) { _, message in
.onChange(of: viewModel.errorMessage) { message in
if let message { toastCenter.show(message) }
}
}
@ -163,7 +163,7 @@ struct QueueManagementView: View {
ProgressView("加载中...")
.frame(maxWidth: .infinity, minHeight: 240)
} else if viewModel.items.isEmpty {
ContentUnavailableView(viewModel.selectedListType.emptyText, systemImage: "person.crop.circle.badge.questionmark")
AppContentUnavailableView(viewModel.selectedListType.emptyText, systemImage: "person.crop.circle.badge.questionmark")
.frame(maxWidth: .infinity, minHeight: 240)
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
} else {

View File

@ -9,12 +9,12 @@ import SwiftUI
///
struct ScenicQueueSettingsView: View {
@Environment(AccountContext.self) private var accountContext
@Environment(ScenicSpotContext.self) private var scenicSpotContext
@Environment(ScenicQueueAPI.self) private var queueAPI
@Environment(ScenicQueueRuntime.self) private var queueRuntime
@Environment(ToastCenter.self) private var toastCenter
@State private var viewModel = ScenicQueueSettingsViewModel()
@EnvironmentObject private var accountContext: AccountContext
@EnvironmentObject private var scenicSpotContext: ScenicSpotContext
@Environment(\.scenicQueueAPI) private var queueAPI
@EnvironmentObject private var queueRuntime: ScenicQueueRuntime
@EnvironmentObject private var toastCenter: ToastCenter
@StateObject private var viewModel = ScenicQueueSettingsViewModel()
@State private var selectedTab: ScenicQueueSettingsTab = .basic
@State private var qrcodeURL: String?
@State private var saving = false
@ -75,7 +75,7 @@ struct ScenicQueueSettingsView: View {
await viewModel.load(api: queueAPI, scenicId: currentScenicId, userId: currentUserId, spots: scenicSpotContext.spots)
await viewModel.loadSettingChangeLogs(api: queueAPI, scenicId: currentScenicId)
}
.onChange(of: viewModel.message) { _, message in
.onChange(of: viewModel.message) { message in
if let message { toastCenter.show(message) }
}
}
@ -106,7 +106,7 @@ struct ScenicQueueSettingsView: View {
DatePicker("营业开始", selection: $viewModel.businessStartTime, displayedComponents: .hourAndMinute)
DatePicker("营业结束", selection: $viewModel.businessEndTime, displayedComponents: .hourAndMinute)
field("最大排队范围", text: $viewModel.maxQueueRangeKm, keyboard: .decimalPad, unit: "公里")
.onChange(of: viewModel.maxQueueRangeKm) { _, value in viewModel.updateMaxQueueRange(value) }
.onChange(of: viewModel.maxQueueRangeKm) { value in viewModel.updateMaxQueueRange(value) }
field("排队次数限制", text: $viewModel.localQueueLimit, keyboard: .numberPad, unit: "")
field("过号顺延", text: $viewModel.localPassDelay, keyboard: .numberPad, unit: "")
field("拍摄分钟", text: $viewModel.photoEstimateMin, keyboard: .numberPad, unit: "")
@ -192,7 +192,7 @@ struct ScenicQueueSettingsView: View {
}
}
if viewModel.logs.isEmpty {
ContentUnavailableView("暂无配置日志", systemImage: "clock.arrow.circlepath")
AppContentUnavailableView("暂无配置日志", systemImage: "clock.arrow.circlepath")
.frame(maxWidth: .infinity, minHeight: 180)
} else {
ForEach(viewModel.logs) { log in