新增景区排队管理功能
This commit is contained in:
@ -0,0 +1,496 @@
|
||||
//
|
||||
// ScenicQueueSettingsViewModel.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 排队设置页 ViewModel,对齐 Android `ScenicQueueSettingsViewModel`。
|
||||
final class ScenicQueueSettingsViewModel {
|
||||
private enum Limits {
|
||||
static let remarkMaxLen = 255
|
||||
static let shootMinute = 0...30
|
||||
static let shootSecond = 0...59
|
||||
static let ahead = 0...50
|
||||
static let broadcastInterval = 40...60
|
||||
static let readableThreshold = 10...30
|
||||
static let queueDistance = 0...9_999_990
|
||||
static let queueTakeLimit = 0...999_999
|
||||
static let missCallRequeueOffset = 1...9_999
|
||||
static let autoCallAheadCount = 0...5
|
||||
}
|
||||
|
||||
private(set) var punchSpotOptions: [ScenicQueuePunchSpotOption] = []
|
||||
private(set) var selectedPunchSpotId: String?
|
||||
private(set) var selectedPunchSpotLabel: String
|
||||
private(set) var shootMinute: Int
|
||||
private(set) var shootSecond: Int
|
||||
private(set) var firstAheadCount: Int
|
||||
private(set) var firstSms: Bool
|
||||
private(set) var firstPhone: Bool
|
||||
private(set) var secondAheadCount: Int
|
||||
private(set) var secondSms: Bool
|
||||
private(set) var secondPhone: Bool
|
||||
private(set) var broadcastIntervalSec: Int
|
||||
private(set) var countdownThresholdSec: Int
|
||||
private(set) var queueDistanceMeter: Int
|
||||
private(set) var queueTakeLimit: Int
|
||||
private(set) var missCallRequeueOffset: Int
|
||||
private(set) var showStartShootingButton: Bool
|
||||
private(set) var autoCallAheadCount: Int
|
||||
private(set) var quickCallButtonEnabled: Bool
|
||||
private(set) var prepareCallButtonEnabled: Bool
|
||||
private(set) var businessOpen: Bool
|
||||
private(set) var businessStartTime: String
|
||||
private(set) var businessEndTime: String
|
||||
private(set) var customTTSText: String
|
||||
private(set) var presetVoices: [String]
|
||||
private(set) var settingsTab: ScenicQueueSettingsTab = .basic
|
||||
private(set) var highlightedInvalidFieldKeys: Set<String> = []
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onNavigateBack: (() -> Void)?
|
||||
var onNavigateLog: (() -> Void)?
|
||||
|
||||
private let appStore: AppStore
|
||||
private let ttsManager: ScenicQueueTTSManaging
|
||||
private var punchSpotSettingRequestGeneration = 0
|
||||
private var ttsLoopAnchorText: String?
|
||||
|
||||
init(appStore: AppStore = .shared, ttsManager: ScenicQueueTTSManaging = AliyunNLSTTSManager.shared) {
|
||||
self.appStore = appStore
|
||||
self.ttsManager = ttsManager
|
||||
let snapshot = appStore.scenicQueueSettingsSnapshot() ?? ScenicQueueSettingsSnapshot()
|
||||
selectedPunchSpotId = appStore.scenicQueuePunchSpotId.nonEmptyTrimmed
|
||||
selectedPunchSpotLabel = appStore.scenicQueuePunchSpotName
|
||||
shootMinute = snapshot.shootMinute.clamped(to: Limits.shootMinute)
|
||||
shootSecond = snapshot.shootSecond.clamped(to: Limits.shootSecond)
|
||||
firstAheadCount = snapshot.firstAheadCount.clamped(to: Limits.ahead)
|
||||
firstSms = snapshot.firstSms
|
||||
firstPhone = snapshot.firstPhone
|
||||
secondAheadCount = snapshot.secondAheadCount.clamped(to: Limits.ahead)
|
||||
secondSms = snapshot.secondSms
|
||||
secondPhone = snapshot.secondPhone
|
||||
broadcastIntervalSec = snapshot.broadcastIntervalSec.clamped(to: Limits.broadcastInterval)
|
||||
countdownThresholdSec = snapshot.countdownThresholdSec.clamped(to: Limits.readableThreshold)
|
||||
queueDistanceMeter = snapshot.queueDistanceMeter.clamped(to: Limits.queueDistance)
|
||||
queueTakeLimit = snapshot.queueTakeLimit.clamped(to: Limits.queueTakeLimit)
|
||||
missCallRequeueOffset = snapshot.missCallRequeueOffset.clamped(to: Limits.missCallRequeueOffset)
|
||||
showStartShootingButton = snapshot.showStartShootingButton
|
||||
autoCallAheadCount = snapshot.autoCallAheadCount.clamped(to: Limits.autoCallAheadCount)
|
||||
quickCallButtonEnabled = snapshot.quickCallButtonEnabled
|
||||
prepareCallButtonEnabled = snapshot.prepareCallButtonEnabled
|
||||
businessOpen = snapshot.businessOpen
|
||||
businessStartTime = Self.normalizedTime(snapshot.businessStartTime, fallback: "10:00")
|
||||
businessEndTime = Self.normalizedTime(snapshot.businessEndTime, fallback: "20:00")
|
||||
customTTSText = appStore.scenicQueueRemark
|
||||
presetVoices = appStore.scenicQueuePresetVoices()
|
||||
}
|
||||
|
||||
var selectedPunchSpotDisplayText: String {
|
||||
if let selectedPunchSpotId,
|
||||
let option = punchSpotOptions.first(where: { $0.id == selectedPunchSpotId }),
|
||||
!option.label.isEmpty {
|
||||
return option.label
|
||||
}
|
||||
if !selectedPunchSpotLabel.isEmpty {
|
||||
return selectedPunchSpotLabel
|
||||
}
|
||||
if let selectedPunchSpotId, !selectedPunchSpotId.isEmpty {
|
||||
return "打卡点(ID \(selectedPunchSpotId))"
|
||||
}
|
||||
return "请选择打卡点"
|
||||
}
|
||||
|
||||
func openSettingChangeLog() {
|
||||
onNavigateLog?()
|
||||
}
|
||||
|
||||
/// 打开打卡点选择器前加载全部打卡点。
|
||||
func loadPunchSpotOptions(api: ScenicQueueAPIProtocol) async {
|
||||
guard appStore.currentScenicId > 0 else {
|
||||
onShowMessage?("请先选择景区")
|
||||
return
|
||||
}
|
||||
do {
|
||||
let response = try await api.scenicSpotListAll(scenicId: String(appStore.currentScenicId))
|
||||
punchSpotOptions = response.list.map {
|
||||
ScenicQueuePunchSpotOption(id: String($0.id), label: $0.name)
|
||||
}
|
||||
if punchSpotOptions.isEmpty {
|
||||
onShowMessage?("暂无可选打卡点")
|
||||
}
|
||||
notifyStateChange()
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 选择打卡点并尝试拉取服务端设置。
|
||||
func setPunchSpot(_ option: ScenicQueuePunchSpotOption, api: ScenicQueueAPIProtocol) async {
|
||||
selectedPunchSpotId = option.id
|
||||
selectedPunchSpotLabel = option.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
highlightedInvalidFieldKeys.remove("punchSpot")
|
||||
notifyStateChange()
|
||||
await fetchQueueSettingAfterPunchSpotSelected(option.id, api: api)
|
||||
}
|
||||
|
||||
func setSettingsTab(_ tab: ScenicQueueSettingsTab) {
|
||||
settingsTab = tab
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func setShootMinute(_ value: Int) { shootMinute = value.clamped(to: Limits.shootMinute); notifyStateChange() }
|
||||
func setShootSecond(_ value: Int) { shootSecond = value.clamped(to: Limits.shootSecond); notifyStateChange() }
|
||||
func setFirstAheadCount(_ value: Int) { firstAheadCount = value.clamped(to: Limits.ahead); notifyStateChange() }
|
||||
func setSecondAheadCount(_ value: Int) { secondAheadCount = value.clamped(to: Limits.ahead); notifyStateChange() }
|
||||
func setBroadcastIntervalSec(_ value: Int) { broadcastIntervalSec = value.clamped(to: Limits.broadcastInterval); notifyStateChange() }
|
||||
func setCountdownThresholdSec(_ value: Int) { countdownThresholdSec = value.clamped(to: Limits.readableThreshold); notifyStateChange() }
|
||||
func setQueueDistanceMeter(_ value: Int) { queueDistanceMeter = value.clamped(to: Limits.queueDistance); notifyStateChange() }
|
||||
func setQueueTakeLimit(_ value: Int) { queueTakeLimit = value.clamped(to: Limits.queueTakeLimit); notifyStateChange() }
|
||||
func setMissCallRequeueOffset(_ value: Int) { missCallRequeueOffset = value.clamped(to: Limits.missCallRequeueOffset); notifyStateChange() }
|
||||
func setAutoCallAheadCount(_ value: Int) { autoCallAheadCount = value.clamped(to: Limits.autoCallAheadCount); notifyStateChange() }
|
||||
func toggleShowStartShootingButton() { showStartShootingButton.toggle(); notifyStateChange() }
|
||||
func toggleQuickCallButtonEnabled() { quickCallButtonEnabled.toggle(); notifyStateChange() }
|
||||
func togglePrepareCallButtonEnabled() { prepareCallButtonEnabled.toggle(); notifyStateChange() }
|
||||
func toggleBusinessOpen() { businessOpen.toggle(); notifyStateChange() }
|
||||
func toggleFirstSms() { firstSms.toggle(); notifyStateChange() }
|
||||
func toggleFirstPhone() { firstPhone.toggle(); notifyStateChange() }
|
||||
func toggleSecondSms() { secondSms.toggle(); notifyStateChange() }
|
||||
func toggleSecondPhone() { secondPhone.toggle(); notifyStateChange() }
|
||||
|
||||
func setBusinessStartTime(_ value: String) {
|
||||
businessStartTime = Self.normalizedTime(value, fallback: businessStartTime)
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func setBusinessEndTime(_ value: String) {
|
||||
businessEndTime = Self.normalizedTime(value, fallback: businessEndTime)
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func setCustomTTSText(_ value: String) {
|
||||
customTTSText = String(value.prefix(Limits.remarkMaxLen))
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 播放或停止自定义 TTS 文案。
|
||||
func toggleCustomTTSPlayback() {
|
||||
if ttsManager.customTextLooping {
|
||||
ttsManager.stopCustomTextLoop()
|
||||
ttsLoopAnchorText = nil
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
let text = customTTSText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !text.isEmpty else {
|
||||
onShowMessage?("请输入要播报的文字")
|
||||
return
|
||||
}
|
||||
Task {
|
||||
let ok = await ttsManager.startCustomTextLoop(text)
|
||||
if ok {
|
||||
ttsLoopAnchorText = text
|
||||
} else {
|
||||
ttsLoopAnchorText = nil
|
||||
onShowMessage?("播报失败,请检查网络与语音服务")
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
/// 播放蓝牙测试音。
|
||||
func playBluetoothTestSound() {
|
||||
Task {
|
||||
let ok = await ttsManager.speakPlainTextOnce("蓝牙音响测试音,请确认外放音量是否合适。")
|
||||
if !ok {
|
||||
onShowMessage?("测试音播放失败,请检查网络与语音服务")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存当前文案到本地预设。
|
||||
func saveCurrentTextAsPresetVoiceLocal() {
|
||||
guard appStore.currentScenicId > 0, selectedPunchSpotId?.nonEmptyTrimmed != nil else {
|
||||
onShowMessage?("请先选择打卡点")
|
||||
return
|
||||
}
|
||||
let text = customTTSText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !text.isEmpty else {
|
||||
onShowMessage?("请输入要播报的文字")
|
||||
return
|
||||
}
|
||||
if presetVoices.count >= 5 {
|
||||
onShowMessage?("最多保存5个语音")
|
||||
return
|
||||
}
|
||||
if presetVoices.contains(text) {
|
||||
onShowMessage?("该文案已在列表中")
|
||||
return
|
||||
}
|
||||
presetVoices.append(text)
|
||||
appStore.saveScenicQueuePresetVoices(presetVoices)
|
||||
onShowMessage?("已保存到本地")
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func deletePresetVoice(at index: Int) {
|
||||
guard presetVoices.indices.contains(index) else { return }
|
||||
let removed = presetVoices.remove(at: index)
|
||||
appStore.saveScenicQueuePresetVoices(presetVoices)
|
||||
if ttsLoopAnchorText == removed {
|
||||
ttsManager.stopCustomTextLoop()
|
||||
ttsLoopAnchorText = nil
|
||||
}
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 保存排队打卡点二维码到相册。
|
||||
func saveShootQueueQRCode(api: ScenicQueueAPIProtocol) async {
|
||||
guard let ids = currentScenicAndSpotIdsForSelected() else {
|
||||
onShowMessage?("请先选择打卡点")
|
||||
return
|
||||
}
|
||||
do {
|
||||
let data = try await api.shootQueueQrcode(scenicId: ids.scenicId, scenicSpotId: ids.scenicSpotId)
|
||||
guard let url = data.qrcodeUrl?.trimmingCharacters(in: .whitespacesAndNewlines), !url.isEmpty else {
|
||||
onShowMessage?("服务端未返回二维码图片地址")
|
||||
return
|
||||
}
|
||||
try await ScenicQueueQRCodeSaver.saveImage(from: url)
|
||||
onShowMessage?("保存成功")
|
||||
} catch {
|
||||
onShowMessage?((error as? LocalizedError)?.errorDescription ?? error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
/// 校验并保存设置。
|
||||
func save(api: ScenicQueueAPIProtocol) async {
|
||||
highlightedInvalidFieldKeys = []
|
||||
guard let ids = currentScenicAndSpotIdsForSelected() else {
|
||||
highlightedInvalidFieldKeys = ["punchSpot"]
|
||||
settingsTab = .basic
|
||||
onShowMessage?("请选择打卡点")
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
let validation = buildSaveValidation()
|
||||
guard validation.messages.isEmpty else {
|
||||
highlightedInvalidFieldKeys = validation.keys
|
||||
settingsTab = tabForValidationKeys(validation.keys)
|
||||
onShowMessage?(validation.messages.joined(separator: "\n"))
|
||||
notifyStateChange()
|
||||
return
|
||||
}
|
||||
let remark = customTTSText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let request = ScenicQueueSaveSettingRequest(
|
||||
scenicId: ids.scenicId,
|
||||
scenicSpotId: ids.scenicSpotId,
|
||||
photoEstimateMin: shootMinute,
|
||||
photoEstimateSec: shootSecond,
|
||||
firstNoticeThresholdPos: firstAheadCount,
|
||||
firstNoticeSmsEnabled: firstSms ? 1 : 0,
|
||||
firstNoticeCallEnabled: firstPhone ? 1 : 0,
|
||||
secondNoticeThresholdPos: secondAheadCount,
|
||||
secondNoticeSmsEnabled: secondSms ? 1 : 0,
|
||||
secondNoticeCallEnabled: secondPhone ? 1 : 0,
|
||||
countdownBroadcastIntervalSec: broadcastIntervalSec,
|
||||
countdownReadableThresholdSec: countdownThresholdSec,
|
||||
queueDistanceMeter: queueDistanceMeter,
|
||||
queueTakeLimit: queueTakeLimit,
|
||||
missCallRequeueOffset: missCallRequeueOffset,
|
||||
showStartShootButton: showStartShootingButton ? 1 : 0,
|
||||
autoCallNextCount: autoCallAheadCount,
|
||||
businessStartTime: businessStartTime,
|
||||
businessEndTime: businessEndTime,
|
||||
voiceBroadcasts: presetVoices.enumerated().map { ScenicQueueVoiceBroadcastItem(content: $0.element, sortOrder: $0.offset + 1) },
|
||||
status: businessOpen ? 1 : 0,
|
||||
remark: remark.isEmpty ? nil : remark
|
||||
)
|
||||
do {
|
||||
try await api.saveSetting(request)
|
||||
appStore.scenicQueuePunchSpotId = String(ids.scenicSpotId)
|
||||
appStore.scenicQueuePunchSpotName = selectedPunchSpotLabel
|
||||
appStore.scenicQueueRemark = remark
|
||||
appStore.saveScenicQueuePresetVoices(presetVoices)
|
||||
appStore.saveScenicQueueSettingsSnapshot(currentSnapshot())
|
||||
_ = try? await api.stats(scenicId: ids.scenicId, scenicSpotId: ids.scenicSpotId)
|
||||
onShowMessage?("保存成功")
|
||||
onNavigateBack?()
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchQueueSettingAfterPunchSpotSelected(_ spotId: String, api: ScenicQueueAPIProtocol) async {
|
||||
guard let scenicId = Int64("\(appStore.currentScenicId)"),
|
||||
let scenicSpotId = Int64(spotId),
|
||||
scenicId > 0,
|
||||
scenicSpotId > 0 else { return }
|
||||
punchSpotSettingRequestGeneration += 1
|
||||
let generation = punchSpotSettingRequestGeneration
|
||||
do {
|
||||
let data = try await api.setting(scenicId: scenicId, scenicSpotId: scenicSpotId)
|
||||
guard generation == punchSpotSettingRequestGeneration,
|
||||
selectedPunchSpotId == spotId,
|
||||
data.exists,
|
||||
let setting = data.setting else { return }
|
||||
applyServerSettingToForm(setting)
|
||||
} catch {
|
||||
// Keep local form values if remote setting cannot be loaded.
|
||||
}
|
||||
}
|
||||
|
||||
private func applyServerSettingToForm(_ setting: ScenicQueueSettingItem) {
|
||||
let fallback = ScenicQueueSettingsSnapshot(
|
||||
showStartShootingButton: showStartShootingButton,
|
||||
autoCallAheadCount: autoCallAheadCount,
|
||||
quickCallButtonEnabled: quickCallButtonEnabled,
|
||||
prepareCallButtonEnabled: prepareCallButtonEnabled
|
||||
)
|
||||
let snapshot = setting.toSnapshot(fallback: fallback)
|
||||
apply(snapshot)
|
||||
customTTSText = String((setting.remark ?? "").prefix(Limits.remarkMaxLen))
|
||||
if let voices = setting.voiceBroadcasts {
|
||||
presetVoices = voices
|
||||
.sorted { ($0.sortOrder ?? Int.max) < ($1.sortOrder ?? Int.max) }
|
||||
.compactMap { $0.content?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmptyTrimmed }
|
||||
.prefix(5)
|
||||
.map { $0 }
|
||||
}
|
||||
if let name = setting.scenicSpotName?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmptyTrimmed {
|
||||
selectedPunchSpotLabel = name
|
||||
}
|
||||
highlightedInvalidFieldKeys = []
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
private func apply(_ snapshot: ScenicQueueSettingsSnapshot) {
|
||||
shootMinute = snapshot.shootMinute.clamped(to: Limits.shootMinute)
|
||||
shootSecond = snapshot.shootSecond.clamped(to: Limits.shootSecond)
|
||||
firstAheadCount = snapshot.firstAheadCount.clamped(to: Limits.ahead)
|
||||
firstSms = snapshot.firstSms
|
||||
firstPhone = snapshot.firstPhone
|
||||
secondAheadCount = snapshot.secondAheadCount.clamped(to: Limits.ahead)
|
||||
secondSms = snapshot.secondSms
|
||||
secondPhone = snapshot.secondPhone
|
||||
broadcastIntervalSec = snapshot.broadcastIntervalSec.clamped(to: Limits.broadcastInterval)
|
||||
countdownThresholdSec = snapshot.countdownThresholdSec.clamped(to: Limits.readableThreshold)
|
||||
queueDistanceMeter = snapshot.queueDistanceMeter.clamped(to: Limits.queueDistance)
|
||||
queueTakeLimit = snapshot.queueTakeLimit.clamped(to: Limits.queueTakeLimit)
|
||||
missCallRequeueOffset = snapshot.missCallRequeueOffset.clamped(to: Limits.missCallRequeueOffset)
|
||||
showStartShootingButton = snapshot.showStartShootingButton
|
||||
autoCallAheadCount = snapshot.autoCallAheadCount.clamped(to: Limits.autoCallAheadCount)
|
||||
quickCallButtonEnabled = snapshot.quickCallButtonEnabled
|
||||
prepareCallButtonEnabled = snapshot.prepareCallButtonEnabled
|
||||
businessOpen = snapshot.businessOpen
|
||||
businessStartTime = Self.normalizedTime(snapshot.businessStartTime, fallback: "10:00")
|
||||
businessEndTime = Self.normalizedTime(snapshot.businessEndTime, fallback: "20:00")
|
||||
}
|
||||
|
||||
private func currentSnapshot() -> ScenicQueueSettingsSnapshot {
|
||||
ScenicQueueSettingsSnapshot(
|
||||
shootMinute: shootMinute,
|
||||
shootSecond: shootSecond,
|
||||
firstAheadCount: firstAheadCount,
|
||||
firstSms: firstSms,
|
||||
firstPhone: firstPhone,
|
||||
secondAheadCount: secondAheadCount,
|
||||
secondSms: secondSms,
|
||||
secondPhone: secondPhone,
|
||||
broadcastIntervalSec: broadcastIntervalSec,
|
||||
countdownThresholdSec: countdownThresholdSec,
|
||||
queueDistanceMeter: queueDistanceMeter,
|
||||
queueTakeLimit: queueTakeLimit,
|
||||
missCallRequeueOffset: missCallRequeueOffset,
|
||||
showStartShootingButton: showStartShootingButton,
|
||||
autoCallAheadCount: autoCallAheadCount,
|
||||
quickCallButtonEnabled: quickCallButtonEnabled,
|
||||
prepareCallButtonEnabled: prepareCallButtonEnabled,
|
||||
businessOpen: businessOpen,
|
||||
businessStartTime: businessStartTime,
|
||||
businessEndTime: businessEndTime
|
||||
)
|
||||
}
|
||||
|
||||
private func buildSaveValidation() -> (messages: [String], keys: Set<String>) {
|
||||
var messages: [String] = []
|
||||
var keys: Set<String> = []
|
||||
if !Limits.shootMinute.contains(shootMinute) {
|
||||
messages.append("拍摄时长:分钟须在 0~30,且按 1 分钟递增")
|
||||
keys.insert(ScenicQueueNumericFieldKey.shootMinute.rawValue)
|
||||
}
|
||||
if !Limits.shootSecond.contains(shootSecond) {
|
||||
messages.append("拍摄时长:秒须在 0~59")
|
||||
keys.insert(ScenicQueueNumericFieldKey.shootSecond.rawValue)
|
||||
}
|
||||
if shootMinute == 0 && shootSecond == 0 {
|
||||
messages.append("拍摄时长须大于 0")
|
||||
keys.insert(ScenicQueueNumericFieldKey.shootMinute.rawValue)
|
||||
keys.insert(ScenicQueueNumericFieldKey.shootSecond.rawValue)
|
||||
}
|
||||
if !Limits.broadcastInterval.contains(broadcastIntervalSec) {
|
||||
messages.append("播报间隔时间须在 40~60 秒")
|
||||
keys.insert(ScenicQueueNumericFieldKey.broadcastIntervalSec.rawValue)
|
||||
}
|
||||
if !Limits.readableThreshold.contains(countdownThresholdSec) {
|
||||
messages.append("进入读秒倒计时阈值须在 10~30 秒")
|
||||
keys.insert(ScenicQueueNumericFieldKey.countdownReadableThresholdSec.rawValue)
|
||||
}
|
||||
return (messages, keys)
|
||||
}
|
||||
|
||||
private func tabForValidationKeys(_ keys: Set<String>) -> ScenicQueueSettingsTab {
|
||||
if keys.contains("punchSpot")
|
||||
|| keys.contains(ScenicQueueNumericFieldKey.queueDistanceMeter.rawValue)
|
||||
|| keys.contains(ScenicQueueNumericFieldKey.shootMinute.rawValue)
|
||||
|| keys.contains(ScenicQueueNumericFieldKey.shootSecond.rawValue)
|
||||
|| keys.contains(ScenicQueueNumericFieldKey.queueTakeLimit.rawValue)
|
||||
|| keys.contains(ScenicQueueNumericFieldKey.missCallRequeueOffset.rawValue) {
|
||||
return .basic
|
||||
}
|
||||
if keys.contains(ScenicQueueNumericFieldKey.firstNoticeAhead.rawValue)
|
||||
|| keys.contains(ScenicQueueNumericFieldKey.secondNoticeAhead.rawValue) {
|
||||
return .notify
|
||||
}
|
||||
return .voice
|
||||
}
|
||||
|
||||
private func currentScenicAndSpotIdsForSelected() -> (scenicId: Int64, scenicSpotId: Int64)? {
|
||||
guard let scenicId = Int64("\(appStore.currentScenicId)"), scenicId > 0,
|
||||
let spotIdText = selectedPunchSpotId?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
let scenicSpotId = Int64(spotIdText), scenicSpotId > 0 else { return nil }
|
||||
return (scenicId, scenicSpotId)
|
||||
}
|
||||
|
||||
private static func normalizedTime(_ raw: String, fallback: String) -> String {
|
||||
let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "zh_CN")
|
||||
for format in ["HH:mm", "HH:mm:ss"] {
|
||||
formatter.dateFormat = format
|
||||
if let date = formatter.date(from: text) {
|
||||
formatter.dateFormat = "HH:mm"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
|
||||
private extension Int {
|
||||
func clamped(to range: ClosedRange<Int>) -> Int {
|
||||
Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var nonEmptyTrimmed: String? {
|
||||
isEmpty ? nil : self
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user