- IM列表没显示未读消息数

- 创建行程的时间限制  开始 < 途径 < 结束
- 行程列表的显示格式调整
This commit is contained in:
linshujie 2026-09-30 16:08:13 +08:00
parent f8e0d98acf
commit 2fb18d85db
10 changed files with 157 additions and 24 deletions

View File

@ -103,7 +103,6 @@
305A76E22FCA8C7000227D26 /* GroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76252FCA8C7000227D26 /* GroupViewModel.swift */; }; 305A76E22FCA8C7000227D26 /* GroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76252FCA8C7000227D26 /* GroupViewModel.swift */; };
305A76E52FCA8C7000227D26 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76292FCA8C7000227D26 /* HomeViewController.swift */; }; 305A76E52FCA8C7000227D26 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A76292FCA8C7000227D26 /* HomeViewController.swift */; };
305A76E62FCA8C7000227D26 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762B2FCA8C7000227D26 /* LaunchViewController.swift */; }; 305A76E62FCA8C7000227D26 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762B2FCA8C7000227D26 /* LaunchViewController.swift */; };
55B2A1013022F0A000784901 /* LaunchPrivacyConsentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2A1003022F0A000784900 /* LaunchPrivacyConsentView.swift */; };
305A76E72FCA8C7000227D26 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762D2FCA8C7000227D26 /* LoginView.swift */; }; 305A76E72FCA8C7000227D26 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762D2FCA8C7000227D26 /* LoginView.swift */; };
305A76E82FCA8C7000227D26 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762E2FCA8C7000227D26 /* LoginViewController.swift */; }; 305A76E82FCA8C7000227D26 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762E2FCA8C7000227D26 /* LoginViewController.swift */; };
305A76E92FCA8C7000227D26 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762F2FCA8C7000227D26 /* LoginViewModel.swift */; }; 305A76E92FCA8C7000227D26 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A762F2FCA8C7000227D26 /* LoginViewModel.swift */; };
@ -347,6 +346,7 @@
55B250083038000100786508 /* MailboxDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250073038000100786507 /* MailboxDetailViewController.swift */; }; 55B250083038000100786508 /* MailboxDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250073038000100786507 /* MailboxDetailViewController.swift */; };
55B26001304B000100786601 /* VersionUpgradePopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B26000304B000100786600 /* VersionUpgradePopVC.swift */; }; 55B26001304B000100786601 /* VersionUpgradePopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B26000304B000100786600 /* VersionUpgradePopVC.swift */; };
55B261010000000100786601 /* PrivacySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B261000000000100786600 /* PrivacySettingsViewController.swift */; }; 55B261010000000100786601 /* PrivacySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B261000000000100786600 /* PrivacySettingsViewController.swift */; };
55B2A1013022F0A000784901 /* LaunchPrivacyConsentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2A1003022F0A000784900 /* LaunchPrivacyConsentView.swift */; };
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; }; 55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
55C1A0113034101000786011 /* PhoneStatusCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0103034101000786010 /* PhoneStatusCollector.swift */; }; 55C1A0113034101000786011 /* PhoneStatusCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0103034101000786010 /* PhoneStatusCollector.swift */; };
55C1A0213034102000786021 /* PrivilegeDeniedOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */; }; 55C1A0213034102000786021 /* PrivilegeDeniedOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */; };
@ -526,7 +526,6 @@
305A76252FCA8C7000227D26 /* GroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewModel.swift; sourceTree = "<group>"; }; 305A76252FCA8C7000227D26 /* GroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupViewModel.swift; sourceTree = "<group>"; };
305A76292FCA8C7000227D26 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = "<group>"; }; 305A76292FCA8C7000227D26 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = "<group>"; };
305A762B2FCA8C7000227D26 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; }; 305A762B2FCA8C7000227D26 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; };
55B2A1003022F0A000784900 /* LaunchPrivacyConsentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchPrivacyConsentView.swift; sourceTree = "<group>"; };
305A762D2FCA8C7000227D26 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; }; 305A762D2FCA8C7000227D26 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
305A762E2FCA8C7000227D26 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = "<group>"; }; 305A762E2FCA8C7000227D26 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = "<group>"; };
305A762F2FCA8C7000227D26 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = "<group>"; }; 305A762F2FCA8C7000227D26 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = "<group>"; };
@ -779,6 +778,7 @@
55B250073038000100786507 /* MailboxDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailboxDetailViewController.swift; sourceTree = "<group>"; }; 55B250073038000100786507 /* MailboxDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailboxDetailViewController.swift; sourceTree = "<group>"; };
55B26000304B000100786600 /* VersionUpgradePopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpgradePopVC.swift; sourceTree = "<group>"; }; 55B26000304B000100786600 /* VersionUpgradePopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpgradePopVC.swift; sourceTree = "<group>"; };
55B261000000000100786600 /* PrivacySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewController.swift; sourceTree = "<group>"; }; 55B261000000000100786600 /* PrivacySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewController.swift; sourceTree = "<group>"; };
55B2A1003022F0A000784900 /* LaunchPrivacyConsentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchPrivacyConsentView.swift; sourceTree = "<group>"; };
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; }; 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
55C1A0103034101000786010 /* PhoneStatusCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneStatusCollector.swift; sourceTree = "<group>"; }; 55C1A0103034101000786010 /* PhoneStatusCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneStatusCollector.swift; sourceTree = "<group>"; };
55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivilegeDeniedOverlay.swift; sourceTree = "<group>"; }; 55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivilegeDeniedOverlay.swift; sourceTree = "<group>"; };

View File

@ -14,9 +14,18 @@
<string>wx1e38bba788d55cde</string> <string>wx1e38bba788d55cde</string>
</array> </array>
</dict> </dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>quciklocation</string>
</array>
</dict>
</array> </array>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>quciklocation</string>
<string>baidumap</string> <string>baidumap</string>
<string>qqmap</string> <string>qqmap</string>
<string>iosamap</string> <string>iosamap</string>
@ -30,6 +39,13 @@
<false/> <false/>
<key>NSExceptionDomains</key> <key>NSExceptionDomains</key>
<dict> <dict>
<key>cdn.u8t.cn</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>cdn2.batiao8.com</key> <key>cdn2.batiao8.com</key>
<dict> <dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key> <key>NSExceptionAllowsInsecureHTTPLoads</key>
@ -51,13 +67,6 @@
<key>NSIncludesSubdomains</key> <key>NSIncludesSubdomains</key>
<true/> <true/>
</dict> </dict>
<key>cdn.u8t.cn</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict> </dict>
</dict> </dict>
<key>UIAppFonts</key> <key>UIAppFonts</key>

View File

@ -56,13 +56,13 @@ class BaseViewController: UIViewController {
print(className) print(className)
MobClick.beginLogPageView(className) MobClick.beginLogPageView(className)
// 返回根页面时恢复 tabBar 可见性(UINavigationControllerDelegate 在 pop 时可能漏触发) if navigationController?.transitionCoordinator == nil {
(tabBarController as? MainTabBarController)?.updateTabBarVisibility() (tabBarController as? MainTabBarController)?.updateTabBarVisibility()
}
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
// popToRoot 动画完成后 navigation stack 才减到 1,此时再恢复 tabBar
(tabBarController as? MainTabBarController)?.updateTabBarVisibility() (tabBarController as? MainTabBarController)?.updateTabBarVisibility()
} }

View File

@ -113,6 +113,21 @@ extension MainTabBarController: QuickLocationTabBarDelegate {
// MARK: - UINavigationControllerDelegate // MARK: - UINavigationControllerDelegate
extension MainTabBarController: UINavigationControllerDelegate { extension MainTabBarController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
updateTabBarVisibility() let hideTabBar = viewController !== navigationController.viewControllers.first
if hideTabBar {
customTabBar.isHidden = true
return
}
guard let coordinator = navigationController.transitionCoordinator else {
updateTabBarVisibility()
return
}
coordinator.animate(alongsideTransition: { _ in }, completion: { [weak self] context in
if context.isCancelled {
self?.updateTabBarVisibility()
} else {
self?.customTabBar.isHidden = false
}
})
} }
} }

View File

@ -68,6 +68,7 @@ final class GroupChatVC: BaseViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
viewModel.isChatActive = true
fd_interactivePopDisabled = true fd_interactivePopDisabled = true
bindViewModel() bindViewModel()
reactiveAction() reactiveAction()
@ -93,6 +94,7 @@ final class GroupChatVC: BaseViewController {
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
viewModel.isChatActive = true
setupKeyboard() setupKeyboard()
IQKeyboardManager.shared.isEnabled = false IQKeyboardManager.shared.isEnabled = false
IQKeyboardManager.shared.resignOnTouchOutside = false IQKeyboardManager.shared.resignOnTouchOutside = false
@ -101,6 +103,7 @@ final class GroupChatVC: BaseViewController {
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
viewModel.markAsRead() viewModel.markAsRead()
viewModel.isChatActive = false
VoicePlayerManager.shared.stop() VoicePlayerManager.shared.stop()
if isVoiceRecording { if isVoiceRecording {
finishVoiceRecording(action: .discard) finishVoiceRecording(action: .discard)
@ -114,6 +117,9 @@ final class GroupChatVC: BaseViewController {
override func viewDidDisappear(_ animated: Bool) { override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
if isMovingFromParent || isBeingDismissed {
removeMessageListener()
}
} }
// MARK: - Keyboard // MARK: - Keyboard
@ -428,6 +434,12 @@ final class GroupChatVC: BaseViewController {
OIMManager.callbacker.addAdvancedMsgListener(listener: msgListener!) OIMManager.callbacker.addAdvancedMsgListener(listener: msgListener!)
} }
private func removeMessageListener() {
guard let listener = msgListener else { return }
OIMManager.callbacker.removeAdvancedMsgListener(listener: listener)
msgListener = nil
}
// MARK: - Long press (quote / revoke / avatar @) // MARK: - Long press (quote / revoke / avatar @)
private func setupChatLongPress() { private func setupChatLongPress() {
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleChatLongPress(_:))) let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleChatLongPress(_:)))

View File

@ -710,8 +710,11 @@ final class GroupChatViewModel {
messagesSubject.accept([]) messagesSubject.accept([])
} }
/// 仅当前聊天页在前台时为 true,避免退出后仍把新消息标成已读。
var isChatActive = false
func markAsRead() { func markAsRead() {
guard !conversationID.isEmpty else { return } guard isChatActive, !conversationID.isEmpty else { return }
OIMManager.manager.markConversationMessage(asRead: conversationID, OIMManager.manager.markConversationMessage(asRead: conversationID,
onSuccess: nil, onSuccess: nil,
onFailure: nil) onFailure: nil)
@ -735,7 +738,7 @@ final class GroupChatViewModel {
// MARK: - Receive // MARK: - Receive
func onReceiveMessage(_ msg: OIMMessageInfo) { func onReceiveMessage(_ msg: OIMMessageInfo) {
cacheMessage(msg) cacheMessage(msg)
if msg.sendID != AppContextManager.shared.userId { if isChatActive, msg.sendID != AppContextManager.shared.userId {
markAsRead() markAsRead()
} }
guard let item = toSectionItem(msg) else { return } guard let item = toSectionItem(msg) else { return }

View File

@ -513,7 +513,16 @@ final class GroupItineraryCell: UITableViewCell {
let minutes = max(1, (end.expected_timestamp - start.expected_timestamp) / 60_000) let minutes = max(1, (end.expected_timestamp - start.expected_timestamp) / 60_000)
let hours = minutes / 60 let hours = minutes / 60
let remainder = minutes % 60 let remainder = minutes % 60
return hours > 0 ? "\(hours)小时\(remainder)分" : "\(minutes)分钟" switch (hours > 0, remainder > 0) {
case (true, true):
return "\(hours)小时\(remainder)分"
case (true, false):
return "\(hours)小时"
case (false, true):
return "\(remainder)分钟"
case (false, false):
return "--"
}
} }
} }

View File

@ -59,6 +59,7 @@ final class GroupViewController: BaseViewController {
private var hasSetupIMListeners = false private var hasSetupIMListeners = false
private var hasRequestedIMData = false private var hasRequestedIMData = false
private var isRequestingIMData = false private var isRequestingIMData = false
private var conversationListRequestID = UUID()
/// 进入页面加载 IM 会话:圈子集合已由业务接口给出,这里只补未读/最后一条 /// 进入页面加载 IM 会话:圈子集合已由业务接口给出,这里只补未读/最后一条
private func loadIMData(showsLoading: Bool = false) { private func loadIMData(showsLoading: Bool = false) {
@ -92,6 +93,7 @@ final class GroupViewController: BaseViewController {
GroupIMService.shared.setConversationListener({ [weak self] conversations in GroupIMService.shared.setConversationListener({ [weak self] conversations in
DispatchQueue.main.async { DispatchQueue.main.async {
self?.conversationListRequestID = UUID()
self?.viewModel.updateConversations(conversations) self?.viewModel.updateConversations(conversations)
} }
}, onNeedsRefresh: { [weak self] in }, onNeedsRefresh: { [weak self] in
@ -110,12 +112,15 @@ final class GroupViewController: BaseViewController {
} }
private func refreshIMData(showsLoading: Bool = false) { private func refreshIMData(showsLoading: Bool = false) {
let requestID = UUID()
conversationListRequestID = requestID
GroupIMService.shared.getConversationList { [weak self] conversations in GroupIMService.shared.getConversationList { [weak self] conversations in
DispatchQueue.main.async { DispatchQueue.main.async {
if showsLoading { if showsLoading {
DLToast.dismiss() DLToast.dismiss()
} }
self?.viewModel.loadConversations(conversations) guard let self, self.conversationListRequestID == requestID else { return }
self.viewModel.loadConversations(conversations)
} }
} onFailure: { } onFailure: {
DispatchQueue.main.async { DispatchQueue.main.async {

View File

@ -72,17 +72,32 @@ final class GroupViewModel {
/// 仅更新会话元数据(未读、时间)- 消息监听回调(只返回变更的会话,需合并) /// 仅更新会话元数据(未读、时间)- 消息监听回调(只返回变更的会话,需合并)
func updateConversations(_ conversations: [OIMConversationInfo]) { func updateConversations(_ conversations: [OIMConversationInfo]) {
var merged: [String: OIMConversationInfo] = [:] var merged: [String: OIMConversationInfo] = [:]
for c in cachedConversations { if let gid = c.groupID { merged[gid] = c } } for c in cachedConversations {
for c in conversations { if let gid = c.groupID { merged[gid] = c } } if let key = Self.conversationGroupKey(c) { merged[key] = c }
}
for c in conversations {
if let key = Self.conversationGroupKey(c) { merged[key] = c }
}
cachedConversations = Array(merged.values) cachedConversations = Array(merged.values)
reloadData() reloadData()
} }
private static func conversationGroupKey(_ conversation: OIMConversationInfo) -> String? {
if let groupID = conversation.groupID, !groupID.isEmpty {
return groupID
}
guard let conversationID = conversation.conversationID, conversationID.hasPrefix("sg_"),
conversationID.count > 3 else { return nil }
return String(conversationID.dropFirst(3))
}
private func reloadData() { private func reloadData() {
let convMap: [String: OIMConversationInfo] = { let convMap: [String: OIMConversationInfo] = {
var m: [String: OIMConversationInfo] = [:] var m: [String: OIMConversationInfo] = [:]
for c in cachedConversations where c.groupID != nil && !(c.groupID?.isEmpty ?? true) { for c in cachedConversations {
m[c.groupID!] = c if let key = Self.conversationGroupKey(c) {
m[key] = c
}
} }
return m return m
}() }()

View File

@ -214,11 +214,27 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
private func showTimePicker(for pointIndex: Int) { private func showTimePicker(for pointIndex: Int) {
view.endEditing(true) view.endEditing(true)
let points = viewModel.pointsRelay.value
guard pointIndex < points.count else { return }
let day = viewModel.selectedDate.value
let bounds = timePickerBounds(for: pointIndex, in: points, on: day)
guard bounds.canSelect else {
DLToast.show(text: "行程时间需满足:开始 < 途径 < 结束")
return
}
let picker = BRDatePickerView(pickerMode: .HM) let picker = BRDatePickerView(pickerMode: .HM)
picker.title = "选择到达时间" picker.title = "选择到达时间"
if pointIndex < viewModel.pointsRelay.value.count, picker.minDate = bounds.min
let existing = viewModel.pointsRelay.value[pointIndex].expectedTime { picker.maxDate = bounds.max
picker.selectDate = existing if let existing = points[pointIndex].expectedTime,
let onDay = combining(timeFrom: existing, on: day) {
picker.selectDate = clamp(onDay, min: bounds.min, max: bounds.max)
} else if let minDate = bounds.min {
picker.selectDate = minDate
} else if let maxDate = bounds.max {
picker.selectDate = maxDate
} }
picker.pickerStyle = makeSchedulePickerStyle() picker.pickerStyle = makeSchedulePickerStyle()
picker.pickerFooterView = makeSchedulePickerFooter { [weak picker] in picker.pickerFooterView = makeSchedulePickerFooter { [weak picker] in
@ -242,6 +258,52 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
showSchedulePicker(picker) showSchedulePicker(picker)
} }
/// 当前点必须晚于前方已选时间、早于后方已选时间(开始 < 途径 < 结束)。
private func timePickerBounds(for index: Int, in points: [SchedulePointItem], on day: Date) -> (min: Date?, max: Date?, canSelect: Bool) {
let cal = Calendar.current
let previous = points.prefix(index).reversed().compactMap(\.expectedTime).first
let next = points.dropFirst(index + 1).compactMap(\.expectedTime).first
var minDate: Date?
if let previous, let onDay = combining(timeFrom: previous, on: day) {
guard let candidate = cal.date(byAdding: .minute, value: 1, to: onDay),
cal.isDate(candidate, inSameDayAs: day) else {
return (nil, nil, false)
}
minDate = candidate
}
var maxDate: Date?
if let next, let onDay = combining(timeFrom: next, on: day) {
guard let candidate = cal.date(byAdding: .minute, value: -1, to: onDay),
cal.isDate(candidate, inSameDayAs: day) else {
return (nil, nil, false)
}
maxDate = candidate
}
if let minDate, let maxDate, minDate > maxDate {
return (minDate, maxDate, false)
}
return (minDate, maxDate, true)
}
private func combining(timeFrom source: Date, on day: Date) -> Date? {
let cal = Calendar.current
var comps = cal.dateComponents([.year, .month, .day], from: day)
let time = cal.dateComponents([.hour, .minute], from: source)
comps.hour = time.hour
comps.minute = time.minute
comps.second = 0
return cal.date(from: comps)
}
private func clamp(_ date: Date, min: Date?, max: Date?) -> Date {
if let min, date < min { return min }
if let max, date > max { return max }
return date
}
private func showLocationPicker(for pointIndex: Int, item: SchedulePointItem) { private func showLocationPicker(for pointIndex: Int, item: SchedulePointItem) {
let coord = item.latitude != 0 || item.longitude != 0 let coord = item.latitude != 0 || item.longitude != 0
? CLLocationCoordinate2D(latitude: item.latitude, longitude: item.longitude) ? CLLocationCoordinate2D(latitude: item.latitude, longitude: item.longitude)
@ -389,6 +451,9 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
guard points.count >= 2 else { DLToast.show(text: "至少需要两个行程点"); return } guard points.count >= 2 else { DLToast.show(text: "至少需要两个行程点"); return }
guard hasLocation.count == points.count else { DLToast.show(text: "请为每个行程点选择地点"); return } guard hasLocation.count == points.count else { DLToast.show(text: "请为每个行程点选择地点"); return }
guard hasTime.count == points.count else { DLToast.show(text: "请为每个行程点选择到达时间"); return } guard hasTime.count == points.count else { DLToast.show(text: "请为每个行程点选择到达时间"); return }
let times = points.compactMap(\.expectedTime)
let isIncreasing = zip(times, times.dropFirst()).allSatisfy { $0 < $1 }
guard isIncreasing else { DLToast.show(text: "行程时间需满足:开始 < 途径 < 结束"); return }
guard !viewModel.selectedGroupKeys.isEmpty else { DLToast.show(text: "请选择分享的圈子"); return } guard !viewModel.selectedGroupKeys.isEmpty else { DLToast.show(text: "请选择分享的圈子"); return }
// 生成 points 数组 // 生成 points 数组