// // GroupViewController.swift // QuickLocation // import UIKit import RxSwift import RxCocoa import RxDataSources import SDCycleScrollView import OpenIMSDK import ObjectMapper final class GroupViewController: BaseViewController { override var isNavigationBarHidden: Bool { true } fileprivate var rootView: GroupView! private let viewModel = GroupViewModel() /// 行程页当前圈子数据 private var itineraryGroupModel: GroupModel? private var itinerarySchedules: [ScheduleModel] = [] override func loadView() { rootView = GroupView(frame: UIScreen.main.bounds) view = rootView } override func viewDidLoad() { super.viewDidLoad() bindViewModel() reactiveAction() observeTableViews() bindItineraryPage() requestRecommandGroup() requestGroupInfo() guard let config = AppContextManager.shared.systemConfig else { return } rootView.cycleScrollView.imageURLStringsGroup = config.groupBannerList } // MARK: - OpenIM private var hasSetupIMListeners = false /// 进入页面加载 IM 数据:确保登录后并行拉取群列表 + 会话 private func loadIMData() { GroupIMService.shared.ensureLogin { [weak self] success in guard let self = self else { return } guard success else { return } self.setupIMListenersIfNeeded() self.refreshIMData() } } /// 注册 IM 监听(仅一次):会话变化更新未读/时间;群组变化(SDK 同步到新群/退群)刷新列表 private func setupIMListenersIfNeeded() { guard !hasSetupIMListeners else { return } hasSetupIMListeners = true // 监听器回调已包含完整的会话列表,直接使用,避免重复 MJExtension 解析 GroupIMService.shared.setConversationListener { [weak self] conversations in self?.viewModel.updateConversations(conversations) } // 群组同步完成(如刚创建/加入的群)后刷新群列表,解决业务接口先返回、SDK 后同步的时序问题 GroupIMService.shared.setGroupListener { [weak self] in GroupIMService.shared.getJoinedGroups { groups in self?.viewModel.loadIMGroups(groups) } } } private func refreshIMData() { let group = DispatchGroup() var groups: [OIMGroupInfo] = [] var conversations: [OIMConversationInfo] = [] group.enter() GroupIMService.shared.getJoinedGroups { g in groups = g group.leave() } group.enter() GroupIMService.shared.getConversationList { c in conversations = c group.leave() } group.notify(queue: .main) { [weak self] in DLToast.dismiss() self?.viewModel.loadIMGroups(groups, conversations: conversations) } } // MARK: - Bindings private func bindViewModel() { viewModel.output.hotGroups .observe(on: MainScheduler.asyncInstance) .bind(to: rootView.hotGroupsCollectionView.rx.items(dataSource: hotGroupDataSource)) .disposed(by: disposeBag) viewModel.output.createdSections .observe(on: MainScheduler.asyncInstance) .bind(to: rootView.createdTableView.rx.items(dataSource: createdDataSource)) .disposed(by: disposeBag) viewModel.output.joinedSections .observe(on: MainScheduler.asyncInstance) .bind(to: rootView.joinedTableView.rx.items(dataSource: joinedDataSource)) .disposed(by: disposeBag) rootView.hotGroupsCollectionView.rx.modelSelected(GroupInfoModel.self) .subscribe(viewModel.hotGroupCellAction.inputs) .disposed(by: disposeBag) } private func reactiveAction() { rootView.createGroupBtn.rx.tapGesture .subscribe(onNext: { _ in AppRouter.push(Route.createGroup) }) .disposed(by: disposeBag) rootView.joinGroupBtn.rx.tapGesture .subscribe(onNext: { _ in AppRouter.push(Route.joinGroup) }) .disposed(by: disposeBag) // 子 Tab:全部圈子(0=joined) / 我的圈子(1=created) rootView.joinedTabLabel.rx.tapGesture .subscribe(onNext: { [weak self] _ in self?.switchToSegment(0) }) .disposed(by: disposeBag) rootView.createdTabLabel.rx.tapGesture .subscribe(onNext: { [weak self] _ in self?.switchToSegment(1) }) .disposed(by: disposeBag) // 顶栏:圈子 / 行程 rootView.circleTabLabel.rx.tapGesture .subscribe(onNext: { [weak self] _ in self?.rootView.selectTopTab(at: 0) }) .disposed(by: disposeBag) rootView.itineraryTabLabel.rx.tapGesture .subscribe(onNext: { [weak self] _ in guard let self = self else { return } self.rootView.selectTopTab(at: 1) self.refreshItineraryPage() }) .disposed(by: disposeBag) Observable.merge( rootView.createdTableView.rx.modelSelected(GroupCellData.self).asObservable(), rootView.joinedTableView.rx.modelSelected(GroupCellData.self).asObservable() ) .subscribe(onNext: { data in guard let groupId = data.groupInfo.groupID else { return } AppRouter.push(Route.groupChat, userInfo: ["groupId": groupId]) }) .disposed(by: disposeBag) // MQTT join/leave/dismiss 后刷新群列表 NotificationCenter.default.rx.notification(.RefreshIMGroupListNotification) .subscribe(onNext: { [weak self] _ in self?.requestGroupInfo() }) .disposed(by: disposeBag) NotificationCenter.default.rx.notification(.RefreshUserConfigNotification) .subscribe(onNext: { [weak self] _ in self?.requestRecommandGroup() self?.requestGroupInfo() guard let config = AppContextManager.shared.systemConfig else { return } self?.rootView.cycleScrollView.imageURLStringsGroup = config.groupBannerList }) .disposed(by: disposeBag) } private func bindItineraryPage() { let page = rootView.itineraryPage page.onSwitchGroup = { [weak self] in self?.showSwitchGroupPop() } page.onSelectSchedule = { model in AppRouter.push(Route.scheduleDetail, userInfo: ["scheduleJson": model.toJSON()]) } page.onDeleteSchedule = { [weak self] model in self?.deleteSchedule(model) } } private func switchToSegment(_ index: Int) { rootView.selectSegment(at: index) let offset = CGPoint(x: CGFloat(index) * rootView.segmentScrollView.bounds.width, y: 0) rootView.segmentScrollView.setContentOffset(offset, animated: false) } // MARK: - 行程页 private func refreshItineraryPage() { GroupService.groupInfo().subscribe(onNext: { [weak self] response in guard let self = self, let model = response.model else { return } self.itineraryGroupModel = model let current = model.groups.first(where: { $0.group_key == model.default_group_key }) self.rootView.itineraryPage.updateGroupName(current?.name ?? "") self.requestItinerarySchedules(groupKey: model.default_group_key) }).disposed(by: disposeBag) } private func requestItinerarySchedules(groupKey: String) { guard !groupKey.isEmpty else { itinerarySchedules = [] rootView.itineraryPage.reloadSchedules([]) return } ItineraryService.groupScheduleList(groupKey: groupKey) .subscribe(onNext: { [weak self] response in guard let self = self else { return } self.itinerarySchedules = response.list self.rootView.itineraryPage.reloadSchedules(response.list) }) .disposed(by: disposeBag) } private func showSwitchGroupPop() { guard let groupModel = itineraryGroupModel else { refreshItineraryPage() return } GroupListPopView.show(groupModel: groupModel) { [weak self] groupKey in guard let self = self, let key = groupKey else { return } GroupService.operate(opType: "setdefault", requestData: ["group_key": key]) .subscribe(onNext: { [weak self] _ in self?.refreshItineraryPage() NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil) }) .disposed(by: self.disposeBag) } } private func deleteSchedule(_ model: ScheduleModel) { guard !model.id.isEmpty else { return } DLToast.showLoading() ItineraryService.delete(id: model.id) .subscribe(onNext: { [weak self] _ in DLToast.dismiss() guard let self = self else { return } self.itinerarySchedules.removeAll { $0.id == model.id } self.rootView.itineraryPage.reloadSchedules(self.itinerarySchedules) }, onError: { _ in DLToast.dismiss() }) .disposed(by: disposeBag) } // MARK: - 内层 tableView 滚动 private func observeTableViews() { rootView.createdTableView.rx.didScroll .observe(on: MainScheduler.asyncInstance) .subscribe(onNext: { [weak self] in guard let self = self else { return } self.rootView.handleTableViewScroll(self.rootView.createdTableView) }) .disposed(by: disposeBag) rootView.joinedTableView.rx.didScroll .observe(on: MainScheduler.asyncInstance) .subscribe(onNext: { [weak self] in guard let self = self else { return } self.rootView.handleTableViewScroll(self.rootView.joinedTableView) }) .disposed(by: disposeBag) } // MARK: - dataSource private lazy var hotGroupDataSource: RxCollectionViewSectionedReloadDataSource = { RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, model in let cell: HotGroupCell = collectionView.dequeueReusableCell(for: indexPath) cell.configure(model) return cell } }() private lazy var createdDataSource: RxTableViewSectionedReloadDataSource = { RxTableViewSectionedReloadDataSource { _, tableView, indexPath, model in let cell: CircleGroupCell = tableView.dequeueReusableCell(for: indexPath) cell.configure(model) return cell } }() private lazy var joinedDataSource: RxTableViewSectionedReloadDataSource = { RxTableViewSectionedReloadDataSource { _, tableView, indexPath, model in let cell: CircleGroupCell = tableView.dequeueReusableCell(for: indexPath) cell.configure(model) return cell } }() // MARK: - API private func requestRecommandGroup() { GroupService.recommand(count: 5).subscribe(onNext: { response in self.viewModel.loadHotGroupData(response.list) }).disposed(by: disposeBag) } private func requestGroupInfo() { GroupService.groupInfo().subscribe { response in guard let model = response.model else { return } self.viewModel.groupList = model.groups DispatchQueue.global(qos: .default).async { [weak self] in self?.loadIMData() } }.disposed(by: disposeBag) } }