- 首页 行程

- 圈子成员列表 智驾报告 UI
This commit is contained in:
linshujie 2026-06-29 18:33:02 +08:00
parent d3220a0f5a
commit 7cddc4499a
63 changed files with 2318 additions and 46 deletions

View File

@ -194,6 +194,11 @@
30A87A6D2FEF5BA10095E7C6 /* SearchLocationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */; };
30A87A6F2FEF7BE40095E7C6 /* SearchLocationResultVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */; };
30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */; };
30B74B3A2FF2115A00F6744D /* GroupScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B392FF2115A00F6744D /* GroupScheduleView.swift */; };
30B74B3C2FF2117900F6744D /* GroupScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */; };
30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */; };
30B74B432FF2438800F6744D /* GroupMemberListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B422FF2438800F6744D /* GroupMemberListView.swift */; };
30B74B452FF24D1B00F6744D /* GroupMemberListVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B442FF24D1B00F6744D /* GroupMemberListVM.swift */; };
30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */; };
30BAB84F2FCD2FED00C33B5C /* InviteJoinVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */; };
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8502FCD331C00C33B5C /* GroupAPI.swift */; };
@ -476,6 +481,11 @@
30A87A6C2FEF5BA10095E7C6 /* SearchLocationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationVC.swift; sourceTree = "<group>"; };
30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = "<group>"; };
30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultView.swift; sourceTree = "<group>"; };
30B74B392FF2115A00F6744D /* GroupScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleView.swift; sourceTree = "<group>"; };
30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleVC.swift; sourceTree = "<group>"; };
30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVC.swift; sourceTree = "<group>"; };
30B74B422FF2438800F6744D /* GroupMemberListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListView.swift; sourceTree = "<group>"; };
30B74B442FF24D1B00F6744D /* GroupMemberListVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVM.swift; sourceTree = "<group>"; };
30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinView.swift; sourceTree = "<group>"; };
30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinVC.swift; sourceTree = "<group>"; };
30BAB8502FCD331C00C33B5C /* GroupAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupAPI.swift; sourceTree = "<group>"; };
@ -948,6 +958,7 @@
30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */,
30C4C01E2FDC0EA6009215C1 /* GroupInfo */,
307073E42FD18A20004C37CC /* GroupChat */,
30B74B3F2FF2435200F6744D /* GroupMemberList */,
30EFF3A22FD7C58400EB35D4 /* GroupSetting */,
30EFF3B12FD8F19E00EB35D4 /* ReviewMemberList */,
30C4C0172FDBF066009215C1 /* RemoveMember */,
@ -972,6 +983,7 @@
30A87A5C2FEE711C0095E7C6 /* Bubble */,
30CCDE4F2FE2782700F5214A /* SignIn */,
30CCDE562FE39F6B00F5214A /* SOS */,
30B74B382FF2105C00F6744D /* GroupSchedule */,
);
path = Home;
sourceTree = "<group>";
@ -1310,6 +1322,25 @@
path = SearchLocation;
sourceTree = "<group>";
};
30B74B382FF2105C00F6744D /* GroupSchedule */ = {
isa = PBXGroup;
children = (
30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */,
30B74B392FF2115A00F6744D /* GroupScheduleView.swift */,
);
path = GroupSchedule;
sourceTree = "<group>";
};
30B74B3F2FF2435200F6744D /* GroupMemberList */ = {
isa = PBXGroup;
children = (
30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */,
30B74B422FF2438800F6744D /* GroupMemberListView.swift */,
30B74B442FF24D1B00F6744D /* GroupMemberListVM.swift */,
);
path = GroupMemberList;
sourceTree = "<group>";
};
30BAB84B2FCD2FA400C33B5C /* InviteJoin */ = {
isa = PBXGroup;
children = (
@ -1722,6 +1753,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
30B74B452FF24D1B00F6744D /* GroupMemberListVM.swift in Sources */,
30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */,
305A76882FCA8C7000227D26 /* MoyaProvider+Rx.swift in Sources */,
305A76892FCA8C7000227D26 /* Observable+Response.swift in Sources */,
@ -1786,6 +1818,7 @@
305A76A72FCA8C7000227D26 /* NSAttributedString+Extension.swift in Sources */,
30DC18542FD00C4A0041DCD1 /* VipRechargeVM.swift in Sources */,
305A76A82FCA8C7000227D26 /* ObjectMapper+Extension.swift in Sources */,
30B74B3A2FF2115A00F6744D /* GroupScheduleView.swift in Sources */,
305A76A92FCA8C7000227D26 /* Optional+Extension.swift in Sources */,
305A76AA2FCA8C7000227D26 /* Response+ObjectMapper.swift in Sources */,
305A76AB2FCA8C7000227D26 /* ScaleType.swift in Sources */,
@ -1938,6 +1971,7 @@
305A76FB2FCA8C7000227D26 /* EmptyDataSet.swift in Sources */,
30BF300E2FED09CC00D9CB52 /* ScheduleDetailVC.swift in Sources */,
305A76FC2FCA8C7000227D26 /* EmptyDataSetDelegate.swift in Sources */,
30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */,
305A76FD2FCA8C7000227D26 /* EmptyDataSetSource.swift in Sources */,
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */,
30EFF3CD2FDA668A00EB35D4 /* MyProfileView.swift in Sources */,
@ -1953,6 +1987,7 @@
30D87CDD2FDFF07500E958FD /* InteractionView.swift in Sources */,
30BAB8652FCD718A00C33B5C /* JoinGroupView.swift in Sources */,
305A77062FCA8C7000227D26 /* MXScrollViewController.m in Sources */,
30B74B432FF2438800F6744D /* GroupMemberListView.swift in Sources */,
305A77072FCA8C7000227D26 /* Helper.swift in Sources */,
30BF30102FED0C8E00D9CB52 /* ScheduleDetailVM.swift in Sources */,
30DA36BD2FECC5AB008D5A2C /* CreateScheduleVipPopView.swift in Sources */,
@ -1971,6 +2006,7 @@
305A770F2FCA8C7000227D26 /* DLCustomPopVC.swift in Sources */,
30EFF29B2FD668C900EB35D4 /* VoiceRecordView.swift in Sources */,
30CCDE532FE2786600F5214A /* SignInView.swift in Sources */,
30B74B3C2FF2117900F6744D /* GroupScheduleVC.swift in Sources */,
305A77102FCA8C7000227D26 /* DLSheetPopVC.swift in Sources */,
30EFF3D32FDA69F400EB35D4 /* AvatarIconListView.swift in Sources */,
30EFF3B72FD8F86200EB35D4 /* ReviewMemberListVM.swift in Sources */,

View File

@ -82,8 +82,10 @@ extension ItineraryAPI: MultiTargetProtocol {
if !group_key.isEmpty {
params["group_key"] = group_key
}
params["page"] = page
params["limit"] = 20
if page != -1 {
params["page"] = page
params["limit"] = 20
}
return .requestParameters(parameters: params, encoding: URLEncoding())
case let .queryFollowList(id):

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_408@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_408@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1914@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1914@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1987@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1987@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1914@2x(1).png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1914@3x(1).png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1986@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1986@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_408@2x(1).png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_408@3x(1).png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1985@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1985@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1988@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1988@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1900@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1900@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1900@2x(1).png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1900@3x(1).png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Vector@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Vector@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Vector@2x(1).png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Vector@3x(1).png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Union@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Union@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1989@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1989@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -58,7 +58,8 @@ class AppContextManager: NSObject {
var sex: Int {
account?.sex ?? -1
}
/// VIP
/// 1 2 3
var vip: Int {
account?.vip ?? 1
}

View File

@ -69,6 +69,10 @@ enum Route: String {
case searchLocation = "searchLocation"
///
case searchLocationResult = "searchLocationResult"
///
case groupSchedule = "groupSchedule"
///
case groupMemberList = "groupMemberList"
}
extension Route: RouterTarget {
@ -323,6 +327,17 @@ extension AppRouter: AppRouterProtocol {
code: parameters["code"].safeInt,
memberData: parameters["memberData"].safeDictionary as! [String : Any])
}
// MARK: -
AppRouter.register(Route.groupSchedule) { url, parameters in
let groupKey = parameters["groupKey"].safeString
return GroupScheduleVC(groupKey: groupKey)
}
// MARK: -
AppRouter.register(Route.groupMemberList) { url, parameters in
GroupMemberListVC(groupKey: parameters["groupKey"].safeString)
}
}
}

View File

@ -183,10 +183,16 @@ final class GroupChatVC: BaseViewController {
})
.disposed(by: disposeBag)
//
rootView.reviewBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.push(Route.reviewMemberList, userInfo: ["groupId": self.viewModel.groupId])
}).disposed(by: disposeBag)
//
rootView.memberBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.push(Route.groupMemberList, userInfo: ["groupKey": self.viewModel.groupId])
}).disposed(by: disposeBag)
//
rootView.voiceBtn.rx.tap.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }

View File

@ -0,0 +1,103 @@
//
// GroupMemberListVC.swift
// QuickLocation
//
// Created by on 2026/6/29.
//
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
import ObjectMapper
class GroupMemberListVC: BaseViewController {
fileprivate var rootView: GroupMemberListView!
override func loadView() {
rootView = GroupMemberListView(frame: UIScreen.main.bounds)
view = rootView
}
private var viewModel: GroupMemberListVM
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
reactiveAction()
requestGroupInfo()
}
private func reactiveAction() {
}
private var selectedRow = 0
private func bindViewModel() {
viewModel.output.sectionedItems
.bind(to: rootView.collectionView.rx.items(dataSource: memberDataSource))
.disposed(by: disposeBag)
viewModel.output.drivingSectionedItems
.bind(to: rootView.drivingEventCV.rx.items(dataSource: drivingDataSource))
.disposed(by: disposeBag)
//
rootView.collectionView.rx.modelSelected(GroupMemberModel.self)
.subscribe(onNext: { [weak self] model in
guard let self = self, let row = self.viewModel.rowOf(userId: model.user_id) else { return }
self.selectedRow = row
self.rootView.selectedMemberIsSelf = self.viewModel.isCurrentUser(id: model.user_id)
self.rootView.collectionView.reloadData()
}).disposed(by: disposeBag)
}
// MARK: - dataSource
private lazy var memberDataSource: RxCollectionViewSectionedReloadDataSource<GroupMemberListSectionModel> = {
RxCollectionViewSectionedReloadDataSource<GroupMemberListSectionModel> { [weak self] datasource, collectionView, indexPath, model in
let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath)
cell.configure(model: model,
isCurrentUser: self?.viewModel.isCurrentUser(id: model.user_id) ?? false,
isSelected: indexPath.row == (self?.selectedRow ?? 0))
return cell
}
}()
private lazy var drivingDataSource: RxCollectionViewSectionedReloadDataSource<DrivingEventSection> = {
RxCollectionViewSectionedReloadDataSource<DrivingEventSection> { _, collectionView, indexPath, item in
let cell: DrivingEventCell = collectionView.dequeueReusableCell(for: indexPath)
cell.configure(item)
return cell
}
}()
// MARK: - API
private func requestGroupInfo() {
DLToast.showLoading()
GroupService.groupInfoByKey(viewModel.groupKey).subscribe { response in
DLToast.dismiss()
guard let model = response.model else { return }
self.viewModel.groupModel = model
self.viewModel.loadData(response.list)
//
if let first = self.viewModel.firstMemberId {
self.rootView.selectedMemberIsSelf = self.viewModel.isCurrentUser(id: first)
}
}.disposed(by: disposeBag)
}
// MARK: - Init
init(groupKey: String) {
viewModel = GroupMemberListVM(groupKey: groupKey)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,130 @@
//
// GroupMemberListVM.swift
// QuickLocation
//
// Created by on 2026/6/29.
//
import RxSwift
import RxRelay
import RxDataSources
import ObjectMapper
// MARK: - DrivingStats
struct DrivingStatsData {
let distance_km: Double?
let frequent_lane_change: Int?
let hard_acceleration: Int?
let hard_braking: Int?
let long_driving: Int?
let low_speeding: Int?
let max_speed: Double?
let period: String?
let sharp_turn: Int?
let signal_loss: Int?
let speeding: Int?
let total: Int?
}
struct DrivingEventItem: IdentifiableType, Equatable {
typealias Identity = String
let identity: String
let title: String
let iconName: String
let count: Int
init(title: String, iconName: String, count: Int = 0) {
self.identity = title
self.title = title
self.iconName = iconName
self.count = count
}
}
typealias DrivingEventSection = SectionModel<String, DrivingEventItem>
class GroupMemberListVM {
let groupKey: String
var groupModel: GroupInfoModel?
struct Output {
var sectionedItems: Observable<[GroupMemberListSectionModel]>
var drivingSectionedItems: Observable<[DrivingEventSection]>
}
let output: Output
private var disposeBag = DisposeBag()
private let sectionedItems = PublishSubject<[GroupMemberListSectionModel]>()
private let drivingItemsRelay = BehaviorRelay<[DrivingEventItem]>(value: GroupMemberListVM.defaultDrivingEvents)
private var memberList: [GroupMemberModel] = []
private static let defaultDrivingEvents: [DrivingEventItem] = [
DrivingEventItem(title: "急加速", iconName: "GroupMemberList/1"),
DrivingEventItem(title: "急转向", iconName: "GroupMemberList/3"),
DrivingEventItem(title: "急刹", iconName: "GroupMemberList/5"),
DrivingEventItem(title: "手机干扰", iconName: "GroupMemberList/7"),
DrivingEventItem(title: "超速", iconName: "GroupMemberList/2"),
DrivingEventItem(title: "低速", iconName: "GroupMemberList/4"),
DrivingEventItem(title: "频繁变道", iconName: "GroupMemberList/6"),
DrivingEventItem(title: "长时间驾驶", iconName: "GroupMemberList/8")
]
//
func isCurrentUser(id: String) -> Bool {
id == AppContextManager.shared.userId
}
//
func isGroupOwn(id: String) -> Bool {
guard let model = groupModel else { return false }
return model.group_key.contains(id)
}
func loadData(_ list: [GroupMemberModel]) {
var tempmemberList = list
tempmemberList.moveToFirst { $0.is_online == true }
tempmemberList.moveToFirst { $0.user_id == AppContextManager.shared.userId }
tempmemberList.moveToFirst { isGroupOwn(id: $0.user_id) }
memberList = tempmemberList
sectionedItems.onNext(memberList.mapSection())
}
/// userId
var firstMemberId: String? { memberList.first?.user_id }
///
func rowOf(userId: String) -> Int? {
memberList.firstIndex(where: { $0.user_id == userId })
}
///
func updateDrivingStats(_ stats: DrivingStatsData) {
let items = Self.defaultDrivingEvents.enumerated().map { i, item in
let count: Int
switch i {
case 0: count = stats.hard_acceleration ?? 0
case 1: count = stats.speeding ?? 0
case 2: count = stats.sharp_turn ?? 0
case 3: count = stats.low_speeding ?? 0
case 4: count = stats.hard_braking ?? 0
case 5: count = stats.frequent_lane_change ?? 0
case 6: count = stats.signal_loss ?? 0
case 7: count = stats.long_driving ?? 0
default: count = 0
}
return DrivingEventItem(title: item.title, iconName: item.iconName, count: count)
}
drivingItemsRelay.accept(items)
}
init(groupKey: String) {
self.groupKey = groupKey
output = Output(
sectionedItems: sectionedItems.asObservable(),
drivingSectionedItems: drivingItemsRelay.map { $0.mapSection() }.asObservable()
)
}
}

View File

@ -0,0 +1,760 @@
//
// GroupMemberListView.swift
// QuickLocation
//
// Created by on 2026/6/29.
//
import UIKit
import RxSwift
import RxCocoa
class GroupMemberListView: UIView {
var disposeBag = DisposeBag()
func updateArrowVisibility() {
let offsetX = collectionView.contentOffset.x
let contentW = collectionView.contentSize.width
let viewW = collectionView.bounds.width
memberArrowLeft.isHidden = offsetX <= 0
memberArrowRight.isHidden = offsetX >= contentW - viewW
}
private func setupRx() {
backBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.shared.popOrDismiss()
}).disposed(by: disposeBag)
collectionView.rx.contentOffset
.subscribe(onNext: { [weak self] _ in
self?.updateArrowVisibility()
})
.disposed(by: disposeBag)
//
selectedDate.subscribe(onNext: { [weak self] date in
guard let self = self else { return }
let fmt = DateFormatter()
fmt.dateFormat = "MM月"
self.monthLab.text = fmt.string(from: date)
}).disposed(by: disposeBag)
// / 7
datePreviousBtn.rx.tap.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
let target = self.dateCollectionView.contentOffset.x - self.dateItemWidth * CGFloat(self.daysPerPage)
self.dateCollectionView.setContentOffset(CGPoint(x: max(0, target), y: 0), animated: true)
}).disposed(by: disposeBag)
dateNextBtn.rx.tap.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
var target = self.dateCollectionView.contentOffset.x + self.dateItemWidth * CGFloat(self.daysPerPage)
let maxOffset = CGFloat(self.dateItems.count) * self.dateItemWidth - self.dateCollectionView.bounds.width
target = min(max(0, maxOffset), target)
self.dateCollectionView.setContentOffset(CGPoint(x: target, y: 0), animated: true)
}).disposed(by: disposeBag)
}
private func setupUI() {
addSubview(navBgView)
addSubview(navBarView)
navBarView.addSubview(navTitleLabel)
navBarView.addSubview(backBtn)
addSubview(memberArrowLeft)
addSubview(collectionView)
addSubview(memberArrowRight)
addSubview(reportView)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160/375)
navBarView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navTitleLabel.layoutChain
.top(kStatusBarHeight + 12)
.centerY(backBtn)
.centerX()
backBtn.layoutChain
.centerY(navTitleLabel)
.left(15)
.width(24)
.height(24)
collectionView.layoutChain
.topToBottomOfView(navBarView, offset: 0)
.height(90)
.edgesHorzontal(28)
memberArrowLeft.layoutChain
.rightToLeftOfView(collectionView, offset: -8)
.height(14)
.width(5)
.centerY(collectionView)
memberArrowRight.layoutChain
.leftToRightOfView(collectionView, offset: 8)
.height(14)
.width(5)
.centerY(collectionView)
reportView.layoutChain
.topToBottomOfView(collectionView, offset: 10)
.edges(excludingEdge: .top)
}
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill
return iv
}()
lazy var navBarView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var navTitleLabel: UILabel = {
let label = UILabel()
label.text = "圈子成员"
label.font = .systemFont(ofSize: 18, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.textAlignment = .center
return label
}()
lazy var backBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100)
return btn
}()
// MARK: -
lazy var memberArrowLeft: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "GroupMemberList/member_left")
view.isHidden = true
return view
}()
lazy var memberArrowRight: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "GroupMemberList/member_right")
view.isHidden = true
return view
}()
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cvWidth = kScreenWidth - 56
layout.itemSize = CGSize(width: 65, height: 90)
layout.minimumLineSpacing = 4
layout.scrollDirection = .horizontal
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .clear
cv.showsHorizontalScrollIndicator = false
cv.register(GroupMemberListCell.self)
return cv
}()
// MARK: -
lazy var reportView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
view.layer.cornerRadius = 20
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
let monthView = UIView()
monthView.backgroundColor = UIColor(hexStr: "#57C7FF")
monthView.cornerRadius = 6
monthView.addSubview(monthLab)
monthLab.layoutChain
.edgesVertical(2)
.edgesHorzontal(12)
view.addSubview(monthView)
monthView.layoutChain
.top(15)
.left(15)
let totalLab = UILabel()
totalLab.text = "本周总计里程"
totalLab.font = .systemFont(ofSize: 10, weight: .medium)
totalLab.textColor = UIColor(hexStr: "#333333")
view.addSubview(totalLab)
totalLab.layoutChain
.leftToRightOfView(monthView, offset: 21)
.centerY(monthView)
view.addSubview(mileageLab)
mileageLab.layoutChain
.leftToRightOfView(totalLab, offset: 5)
.centerY(monthView)
let highLab = UILabel()
highLab.text = "本周最高时速"
highLab.font = .systemFont(ofSize: 10, weight: .medium)
highLab.textColor = UIColor(hexStr: "#333333")
view.addSubview(highLab)
highLab.layoutChain
.leftToRightOfView(mileageLab, offset: 23)
.centerY(monthView)
view.addSubview(speedLab)
speedLab.layoutChain
.leftToRightOfView(highLab, offset: 5)
.centerY(monthView)
view.addSubview(dateView)
dateView.layoutChain
.topToBottomOfView(monthView, offset: 10)
.edgesHorzontal()
view.addSubview(scrollView)
scrollView.layoutChain
.topToBottomOfView(dateView)
.edges(excludingEdge: .top)
return view
}()
lazy var monthLab: UILabel = {
let label = UILabel()
label.text = " "
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#0F2846")
label.textAlignment = .center
return label
}()
///
lazy var mileageLab: UILabel = {
let label = UILabel()
label.text = "0km"
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(hexStr: "#16B3FF")
return label
}()
///
lazy var speedLab: UILabel = {
let label = UILabel()
label.text = "0km/h"
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(hexStr: "#16B3FF")
return label
}()
// MARK: -
private var dateItems: [DateItem] = []
let selectedDate = BehaviorRelay<Date>(value: Date())
private let daysPerPage = 7
///
var selectedMemberIsSelf = true {
didSet { updateDateSelectability() }
}
struct DateItem {
let date: Date
let day: Int
let isToday: Bool
let isFuture: Bool
let isSelectable: Bool
}
/// VIP
private var maxSelectableDays: Int {
switch AppContextManager.shared.vip {
case 1: return 0 //
case 2: return selectedMemberIsSelf ? 7 : 1
case 3: return selectedMemberIsSelf ? 30 : 14
default: return 0
}
}
/// dateItems isSelectable
private func updateDateSelectability() {
let calendar = Calendar.current
let today = Date()
let maxDays = maxSelectableDays
dateItems = dateItems.map { item in
let daysAgo = calendar.dateComponents([.day], from: item.date, to: today).day ?? 0
return DateItem(date: item.date, day: item.day, isToday: item.isToday,
isFuture: item.isFuture,
isSelectable: maxDays > 0 && !item.isFuture && daysAgo <= maxDays - 1)
}
dateCollectionView.reloadData()
}
private func generateDateItems() {
let calendar = Calendar.current
let today = Date()
// 31 3 = 35 5 × 7
let pastDays = 31
let futureDays = 3
let totalDays = pastDays + 1 + futureDays // 35
dateItems = (0..<totalDays).map { i in
let offset = i - pastDays
let date = calendar.date(byAdding: .day, value: offset, to: today) ?? today
let day = calendar.component(.day, from: date)
let isFuture = date > today
let daysAgo = calendar.dateComponents([.day], from: date, to: today).day ?? 0
return DateItem(date: date, day: day,
isToday: calendar.isDateInToday(date),
isFuture: isFuture,
isSelectable: maxSelectableDays > 0 && !isFuture && daysAgo <= maxSelectableDays - 1)
}
}
///
lazy var dateView: UIView = {
let view = UIView()
view.backgroundColor = .clear
generateDateItems()
view.addSubview(datePreviousBtn)
view.addSubview(dateNextBtn)
view.addSubview(dateCollectionView)
datePreviousBtn.layoutChain
.left(15)
.edgesVertical(10)
.width(20)
.height(20)
dateNextBtn.layoutChain
.right(15)
.centerY()
.width(20)
.height(20)
dateCollectionView.layoutChain
.leftToRightOfView(datePreviousBtn, offset: 5)
.rightToLeftOfView(dateNextBtn, offset: -5)
.edgesVertical()
// 4 index 28 3
DispatchQueue.main.async {
let page: CGFloat = 4 // 5 items 28-34
let offset = page * CGFloat(self.daysPerPage) * self.dateItemWidth
self.dateCollectionView.contentOffset.x = offset
}
return view
}()
private var dateItemWidth: CGFloat {
let available = kScreenWidth - 15 - 20 - 5 - 5 - 20 - 15
return floor(available / CGFloat(daysPerPage))
}
lazy var dateCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: dateItemWidth, height: 40)
layout.minimumLineSpacing = 0
layout.scrollDirection = .horizontal
layout.sectionInset = .zero
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .clear
cv.showsHorizontalScrollIndicator = false
cv.isPagingEnabled = true
cv.register(DateCell.self)
cv.dataSource = self
cv.delegate = self
return cv
}()
lazy var datePreviousBtn: UIButton = {
let btn = UIButton()
btn.setImage(UIImage(named: "GroupMemberList/date_left"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 0)
return btn
}()
lazy var dateNextBtn: UIButton = {
let btn = UIButton()
btn.setImage(UIImage(named: "GroupMemberList/date_right"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 0, bottom: 15, right: 15)
return btn
}()
// MARK: -
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.backgroundColor = .clear
view.showsVerticalScrollIndicator = false
view.bounces = false
view.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: kSafeBottomMargin, right: 0)
let contentView = UIView()
contentView.backgroundColor = .clear
view.addSubview(contentView)
contentView.layoutChain.edges().widthToView(view)
contentView.addSubview(drivingAnalysisView)
drivingAnalysisView.layoutChain
.top(5)
.edgesHorzontal(15)
.height(249)
.bottom(20)
return view
}()
lazy var drivingAnalysisView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 10
let titleBg = UIImageView(image: UIImage(named: "GroupMemberList/title_bg"))
view.addSubview(titleBg)
let titleLab = UILabel()
titleLab.text = "驾驶分析"
titleLab.font = .systemFont(ofSize: 16, weight: .medium)
view.addSubview(titleLab)
titleLab.layoutChain
.top(15)
.centerX()
titleBg.layoutChain
.centerX()
.bottomToView(titleLab, offset: 5)
view.addSubview(drivingEventCV)
drivingEventCV.layoutChain
.topToBottomOfView(titleBg, offset: 20)
.edgesHorzontal()
.bottom(20)
return view
}()
lazy var drivingEventCV: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 66, height: 75)
layout.minimumLineSpacing = 15
layout.scrollDirection = .vertical
layout.sectionInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .clear
cv.showsHorizontalScrollIndicator = false
cv.isScrollEnabled = false
cv.register(DrivingEventCell.self)
return cv
}()
override func layoutSubviews() {
super.layoutSubviews()
updateArrowVisibility()
}
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .white
setupUI()
setupRx()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - UICollectionViewDataSource & Delegate ()
extension GroupMemberListView: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
dateItems.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: DateCell = collectionView.dequeueReusableCell(for: indexPath)
let item = dateItems[indexPath.row]
let isSel = Calendar.current.isDate(item.date, inSameDayAs: selectedDate.value)
cell.configure(item: item, isSelected: isSel)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard collectionView == dateCollectionView else { return }
let item = dateItems[indexPath.row]
guard item.isSelectable else { return }
let oldDate = selectedDate.value
selectedDate.accept(item.date)
if let oldRow = dateItems.firstIndex(where: { Calendar.current.isDate($0.date, inSameDayAs: oldDate) }),
let oldCell = dateCollectionView.cellForItem(at: IndexPath(row: oldRow, section: 0)) as? DateCell {
oldCell.configure(item: dateItems[oldRow], isSelected: false)
}
if let newCell = dateCollectionView.cellForItem(at: indexPath) as? DateCell {
newCell.configure(item: item, isSelected: true)
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard scrollView == dateCollectionView else { return }
let targetX = targetContentOffset.pointee.x
let idx = round(targetX / dateItemWidth)
targetContentOffset.pointee.x = idx * dateItemWidth
}
}
// MARK: - DateCell
class DateCell: UICollectionViewCell {
private let bgView: UIView = {
let v = UIView()
v.backgroundColor = UIColor(hexStr: "#16B3FF")
v.cornerRadius = 11
v.isHidden = true
return v
}()
private let dayLab: UILabel = {
let l = UILabel()
l.font = .systemFont(ofSize: 14, weight: .medium)
l.textAlignment = .center
return l
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(bgView)
contentView.addSubview(dayLab)
bgView.layoutChain.centerX().centerY().width(36).height(22)
dayLab.layoutChain.centerX().centerY()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func configure(item: GroupMemberListView.DateItem, isSelected: Bool) {
dayLab.text = "\(item.day)"
let disabled = item.isFuture || !item.isSelectable
bgView.isHidden = !isSelected || disabled
dayLab.textColor = disabled ? UIColor(hexStr: "#D1D1D6")
: isSelected ? .white
: UIColor(hexStr: "#333333")
}
}
// MARK: - GroupMemberListCell
class GroupMemberListCell: UICollectionViewCell {
func configure(model: GroupMemberModel, isCurrentUser: Bool, isSelected: Bool) {
avaterImgView.image = model.userIcon
vipIcon.image = model.vipIcon
nameLab.text = model.nick_name
nameLab.textColor = UIColor(hexStr: isCurrentUser ? "#16B3FF" : "#0F2846")
selectedBgView.isHidden = !isSelected
//
if AppContextManager.shared.vip > 1, model.is_online {
batteryInfoView.isHidden = model.battery.int == 0
// 16
let batteryPercent = min(CGFloat(model.battery.int), 100)
batteryView.layoutChain.width(CGFloat(16 - 1) * batteryPercent / 100.0)
batteryLab.text = "\(model.battery)%"
}
}
private func setupSubviews() {
contentView.addSubview(selectedBgView)
contentView.addSubview(avaterImgView)
contentView.addSubview(vipIcon)
contentView.addSubview(batteryInfoView)
batteryInfoView.addSubview(cornerView)
cornerView.addSubview(batteryView)
cornerView.addSubview(batteryIcon)
cornerView.addSubview(batteryLab)
contentView.addSubview(nameLab)
setupLayout()
}
private func setupLayout() {
selectedBgView.layoutChain.edges()
avaterImgView.layoutChain
.top(11)
.edgesHorzontal(10)
.heightToWidth(1)
batteryInfoView.layoutChain
.leftToView(avaterImgView)
.rightToView(avaterImgView)
.bottomToView(avaterImgView)
.height(12)
cornerView.layoutChain.edges()
batteryIcon.layoutChain
.left(7)
.centerY()
.width(16)
.height(8)
batteryView.layoutChain
.topToView(batteryIcon)
.leftToView(batteryIcon, offset: -1)
.bottomToView(batteryIcon)
batteryLab.layoutChain
.leftToRightOfView(batteryIcon, offset: 4)
.right(5)
.centerY()
vipIcon.layoutChain
.topToView(avaterImgView, offset: -8)
.leftToView(avaterImgView, offset: -6)
.width(25)
.height(21)
nameLab.layoutChain
.topToBottomOfView(batteryInfoView, offset: 4)
.edgesHorzontal()
}
lazy var selectedBgView: UIImageView = {
let view = UIImageView(image: UIImage(named: "GroupMemberList/selected_bg"))
view.contentMode = .scaleAspectFill
view.isHidden = true
return view
}()
lazy var avaterImgView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .lightGray
view.contentMode = .scaleAspectFill
view.cornerRadius = 25
return view
}()
lazy var batteryInfoView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.1).cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 2)
view.layer.shadowOpacity = 1
view.layer.shadowRadius = 6
view.isHidden = true
return view
}()
lazy var cornerView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 6
return view
}()
lazy var batteryView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#75E582")
return view
}()
lazy var batteryIcon: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.image = UIImage(named: "Home/battery")
return view
}()
lazy var batteryLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#D4D4D4")
label.font = .systemFont(ofSize: 6, weight: .medium)
return label
}()
lazy var vipIcon: UIImageView = {
let view = UIImageView()
return view
}()
lazy var nameLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#0F2846")
label.font = .systemFont(ofSize: 12, weight: .medium)
label.textAlignment = .center
return label
}()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - DrivingEventCell
class DrivingEventCell: UICollectionViewCell {
func configure(_ item: DrivingEventItem) {
iconView.image = UIImage(named: item.iconName)
nameLab.text = "\(item.title)\n\(item.count)"
}
private let bgView: UIView = {
let v = UIView()
v.backgroundColor = UIColor(hexStr: "#F5FBFF")
v.cornerRadius = 8
return v
}()
lazy var iconView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
return view
}()
lazy var nameLab: UILabel = {
let l = UILabel()
l.font = .systemFont(ofSize: 12, weight: .regular)
l.textAlignment = .center
l.numberOfLines = 0
return l
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(bgView)
contentView.addSubview(iconView)
contentView.addSubview(nameLab)
bgView.layoutChain
.edges(excludingEdge: .top)
.height(60)
iconView.layoutChain
.top()
.centerX()
.width(32)
.heightToWidth(1)
nameLab.layoutChain
.topToBottomOfView(iconView, offset: 8)
.edgesHorzontal()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}

View File

@ -102,7 +102,7 @@ class GroupListPopView: UIView {
tableView.showsVerticalScrollIndicator = false
tableView.bounces = false
tableView.isScrollEnabled = false
tableView.register(GroupListPopCell.self)
tableView.register(GroupListCell.self)
tableView.tableHeaderView = UIView(frame: CGRectMake(0, 0, kScreenWidth, 10))
tableView.dataSource = self
tableView.delegate = self
@ -273,7 +273,7 @@ extension GroupListPopView: UITableViewDataSource, UITableViewDelegate {
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: GroupListPopCell = tableView.dequeueReusableCell(for: indexPath)
let cell: GroupListCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model: groupList[indexPath.row],
isSelected: defaultGroupKey == groupList[indexPath.row].group_key)
return cell
@ -286,7 +286,7 @@ extension GroupListPopView: UITableViewDataSource, UITableViewDelegate {
// MARK: - GroupListPopCell
class GroupListPopCell: UITableViewCell {
class GroupListCell: UITableViewCell {
func configure(model: GroupInfoModel, isSelected: Bool) {
avaterImgView.image = model.groupIcon

View File

@ -0,0 +1,226 @@
//
// GroupScheduleVC.swift
// QuickLocation
//
// Created by on 2026/6/29.
//
import UIKit
import RxSwift
import RxCocoa
import ObjectMapper
import SwiftyUserDefaults
import AMapNaviKit
import AMapSearchKit
class GroupScheduleVC: BaseViewController {
fileprivate var rootView: GroupScheduleView!
override func loadView() {
rootView = GroupScheduleView(frame: UIScreen.main.bounds)
view = rootView
}
private let groupKey: String
private var scheduleList: [ScheduleModel] = []
private var selectedIndex: Int?
private let routeSearch = AMapSearchAPI()
private var routeOverlays: [MAPolyline] = []
private var pointAnnotations: [MAPointAnnotation] = []
override func viewDidLoad() {
super.viewDidLoad()
rootView.tableView.dataSource = self
rootView.tableView.delegate = self
setupMap()
requestGroupScheduleList()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isMovingFromParent || isBeingDismissed {
rootView.cleanupMap()
}
}
// MARK: - API
private func requestGroupScheduleList() {
DLToast.showLoading()
ItineraryService.groupScheduleList(groupKey: groupKey).subscribe(onNext: { response in
DLToast.dismiss()
guard response.list.count > 0 else {
DLToast.show(text: "暂无行程数据") {
AppRouter.shared.popOrDismiss()
}
return
}
self.scheduleList = response.list
self.rootView.tableView.reloadData()
}).disposed(by: disposeBag)
}
// MARK: - Map
private func setupMap() {
rootView.mapView.delegate = self
routeSearch?.delegate = self
if let lat = Defaults[\.currentLatitude], let lon = Defaults[\.currentLongitude] {
let coord = CLLocationCoordinate2D(latitude: lat, longitude: lon)
if CLLocationCoordinate2DIsValid(coord) {
rootView.mapView.setCenter(coord, animated: false)
rootView.mapView.setZoomLevel(14, animated: false)
}
}
}
private func showScheduleOnMap(_ model: ScheduleModel) {
// 线
for ann in pointAnnotations { rootView.mapView.removeAnnotation(ann) }
for ol in routeOverlays { rootView.mapView.remove(ol) }
pointAnnotations.removeAll()
routeOverlays.removeAll()
//
let validPoints = model.points.filter {
guard let lat = $0.latitude, let lon = $0.longitude else { return false }
return abs(lat) > 0.0001 && abs(lon) > 0.0001
}
for (i, p) in validPoints.enumerated() {
let ann = MAPointAnnotation()
ann.coordinate = CLLocationCoordinate2D(latitude: p.latitude!, longitude: p.longitude!)
ann.title = "\(i + 1)"
rootView.mapView.addAnnotation(ann)
pointAnnotations.append(ann)
}
//
if !pointAnnotations.isEmpty {
rootView.mapView.showAnnotations(pointAnnotations, animated: true)
}
// 线
guard validPoints.count >= 2 else { return }
let request = AMapDrivingRouteSearchRequest()
request.origin = AMapGeoPoint.location(withLatitude: CGFloat(validPoints[0].latitude!),
longitude: CGFloat(validPoints[0].longitude!))
request.destination = AMapGeoPoint.location(withLatitude: CGFloat(validPoints.last!.latitude!),
longitude: CGFloat(validPoints.last!.longitude!))
if validPoints.count > 2 {
var waypoints: [AMapGeoPoint] = []
for i in 1..<validPoints.count - 1 {
if let wp = AMapGeoPoint.location(withLatitude: CGFloat(validPoints[i].latitude ?? 0),
longitude: CGFloat(validPoints[i].longitude ?? 0)) {
waypoints.append(wp)
}
}
request.waypoints = waypoints
}
request.strategy = 0
routeSearch?.aMapDrivingRouteSearch(request)
}
private static func numberImage(_ num: Int) -> UIImage? {
let size = CGSize(width: 20, height: 20)
let rect = CGRect(origin: .zero, size: size)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
guard let ctx = UIGraphicsGetCurrentContext() else { return nil }
ctx.setLineWidth(1)
ctx.setStrokeColor(UIColor.white.cgColor)
ctx.setFillColor(UIColor(hexStr: "#16B3FF").cgColor)
let path = UIBezierPath(ovalIn: rect)
path.fill()
path.stroke()
let text = "\(num)" as NSString
let attrs: [NSAttributedString.Key: Any] = [.font: UIFont.boldSystemFont(ofSize: 11), .foregroundColor: UIColor.white]
let strSize = text.size(withAttributes: attrs)
text.draw(at: CGPoint(x: (size.width - strSize.width) / 2, y: (size.height - strSize.height) / 2))
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
// MARK: - Init
init(groupKey: String) {
self.groupKey = groupKey
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - UITableViewDataSource & Delegate
extension GroupScheduleVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
scheduleList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: GroupScheduleCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(scheduleList[indexPath.row], isSelected: indexPath.row == selectedIndex)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedIndex = indexPath.row
tableView.reloadData()
showScheduleOnMap(scheduleList[indexPath.row])
rootView.dismissPanel()
}
}
// MARK: - MAMapViewDelegate
extension GroupScheduleVC: MAMapViewDelegate {
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
guard !(annotation is MAUserLocation), let pointAnn = annotation as? MAPointAnnotation else { return nil }
if let num = Int(pointAnn.title ?? "") {
let id = "SchedulePin"
var view = mapView.dequeueReusableAnnotationView(withIdentifier: id)
if view == nil { view = MAAnnotationView(annotation: annotation, reuseIdentifier: id) }
else { view?.annotation = annotation }
view?.image = Self.numberImage(num)
view?.centerOffset = CGPoint(x: 0, y: -15)
return view
}
return nil
}
func mapView(_ mapView: MAMapView!, rendererFor overlay: MAOverlay!) -> MAOverlayRenderer! {
if let polyline = overlay as? MAPolyline {
let r = MAPolylineRenderer(polyline: polyline)
r?.strokeColor = UIColor(hexStr: "#16B3FF")
r?.lineWidth = 3
r?.lineDashType = kMALineDashTypeSquare
return r
}
return nil
}
}
// MARK: - AMapSearchDelegate
extension GroupScheduleVC: AMapSearchDelegate {
func onRouteSearchDone(_ request: AMapRouteSearchBaseRequest!, response: AMapRouteSearchResponse!) {
guard let path = response.route?.paths?.first as? AMapPath else { return }
var coords: [CLLocationCoordinate2D] = []
for step in path.steps {
guard let polylineStr = step.polyline else { continue }
for point in polylineStr.components(separatedBy: ";") {
let latLon = point.components(separatedBy: ",")
if latLon.count == 2, let lon = Double(latLon[0]), let lat = Double(latLon[1]) {
coords.append(CLLocationCoordinate2D(latitude: lat, longitude: lon))
}
}
}
guard coords.count > 1 else { return }
var mutableCoords = coords
if let polyline = MAPolyline(coordinates: &mutableCoords, count: UInt(coords.count)) {
rootView.mapView.add(polyline)
routeOverlays.append(polyline)
}
}
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
print("Route error: \(error.localizedDescription)")
}
}

View File

@ -0,0 +1,361 @@
//
// GroupScheduleView.swift
// QuickLocation
//
// Created by on 2026/6/29.
//
import UIKit
import RxSwift
import RxCocoa
import AMapNaviKit
class GroupScheduleView: UIView {
var disposeBag = DisposeBag()
let selectedSchedule = PublishSubject<ScheduleModel>()
// MARK: - PopView
private var popTopConstraint: NSLayoutConstraint?
private var isLimitsSet = false
private let popDownHeight: CGFloat = 250
private var popUpLimit: CGFloat = 0
private var panStartTop: CGFloat = 0
private var isSubCanScroll = false
private func setupRx() {
backBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.shared.popOrDismiss()
}).disposed(by: disposeBag)
}
private func setupUI() {
addSubview(mapView)
addSubview(navBgView)
addSubview(navBarView)
navBarView.addSubview(navTitleLabel)
navBarView.addSubview(backBtn)
addSubview(bottomView)
bottomView.addSubview(lineView)
bottomView.addSubview(tableView)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160/375)
navBarView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navTitleLabel.layoutChain
.top(kStatusBarHeight + 12)
.centerY(backBtn)
.centerX()
backBtn.layoutChain
.centerY(navTitleLabel)
.left(15)
.width(24)
.height(24)
mapView.layoutChain
.top()
.edgesHorzontal()
.bottom()
//
bottomView.layoutChain
.edgesHorzontal()
.bottom()
.top(kScreenHeight - popDownHeight)
lineView.layoutChain
.top(8)
.centerX()
.width(36)
.height(4)
tableView.layoutChain
.topToBottomOfView(lineView, offset: 10)
.edgesHorzontal()
.bottom()
popTopConstraint = bottomView.jh_constraint(
.top, toAttribute: .top, otherView: bottomView.superview, relation: .equal
)
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
pan.delegate = self
bottomView.addGestureRecognizer(pan)
}
// MARK: - Pan Gesture
@objc private func handlePan(_ pan: UIPanGestureRecognizer) {
guard isLimitsSet, let topConstraint = popTopConstraint else { return }
switch pan.state {
case .began:
panStartTop = bottomView.frame.minY
case .changed:
let newTop = panStartTop + pan.translation(in: self).y
let scrollOffset = tableView.contentOffset.y
if isSubCanScroll {
if scrollOffset > 0 { return }
if pan.velocity(in: self).y > 0 || bottomView.frame.minY > popUpLimit + 1 {
isSubCanScroll = false
panStartTop = bottomView.frame.minY
}
} else {
if bottomView.frame.minY <= popUpLimit && newTop <= popUpLimit {
isSubCanScroll = true
panStartTop = bottomView.frame.minY
topConstraint.constant = popUpLimit
return
}
}
let clamped = max(popUpLimit, min(kScreenHeight - popDownHeight, newTop))
topConstraint.constant = clamped
case .ended, .cancelled:
let velocity = pan.velocity(in: self)
let frameMinY = bottomView.frame.minY
let isNearUp = abs(frameMinY - popUpLimit) < abs(frameMinY - (kScreenHeight - popDownHeight))
let target: CGFloat
if frameMinY <= popUpLimit + 5 {
target = isNearUp ? popUpLimit : (kScreenHeight - popDownHeight)
} else if abs(velocity.y) > 200 {
target = velocity.y < 0 ? popUpLimit : (kScreenHeight - popDownHeight)
} else {
target = isNearUp ? popUpLimit : (kScreenHeight - popDownHeight)
}
topConstraint.constant = target
isSubCanScroll = target == popUpLimit
UIView.animate(withDuration: 0.3, delay: 0,
usingSpringWithDamping: 0.85,
initialSpringVelocity: abs(velocity.y) / 1000,
options: [.allowUserInteraction]) {
self.layoutIfNeeded()
}
default:
break
}
}
override func layoutSubviews() {
super.layoutSubviews()
if !isLimitsSet {
isLimitsSet = true
popUpLimit = navBarView.frame.maxY
}
}
// MARK: - Views
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill
return iv
}()
lazy var navBarView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var navTitleLabel: UILabel = {
let label = UILabel()
label.text = "行程路线"
label.font = .systemFont(ofSize: 18, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.textAlignment = .center
return label
}()
lazy var backBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100)
return btn
}()
lazy var mapView: MAMapView! = {
let mv = MAMapView()
mv.zoomLevel = 14
mv.showsUserLocation = false
mv.showsCompass = false
mv.userTrackingMode = .none
return mv
}()
lazy var bottomView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.layer.cornerRadius = 16
v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
return v
}()
lazy var lineView: UIView = {
let v = UIView()
v.backgroundColor = UIColor(hexStr: "#EBEBEB")
v.cornerRadius = 2
return v
}()
lazy var tableView: UITableView = {
let tv = UITableView(frame: .zero, style: .plain)
tv.backgroundColor = .clear
tv.separatorStyle = .none
tv.estimatedRowHeight = 137
tv.rowHeight = UITableView.automaticDimension
tv.register(GroupScheduleCell.self)
return tv
}()
///
func dismissPanel() {
guard let topConstraint = popTopConstraint else { return }
isSubCanScroll = false
topConstraint.constant = kScreenHeight - popDownHeight
UIView.animate(withDuration: 0.3) { self.layoutIfNeeded() }
}
func cleanupMap() {
#if !targetEnvironment(simulator)
mapView?.delegate = nil
mapView?.removeFromSuperview()
mapView = nil
#endif
}
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .white
setupUI()
setupRx()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - UIGestureRecognizerDelegate
extension GroupScheduleView: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
return true
}
}
// MARK: - GroupScheduleCell
class GroupScheduleCell: UITableViewCell {
func configure(_ model: ScheduleModel, isSelected: Bool) {
dateLab.text = "行程时间:\(getDateInterval2String(date: "\(model.timestamp/1000)", dateFormat: "yyyy年MM月dd日"))"
iconView.image = model.userIcon
nameLab.text = "\(model.nick_name) 的行程路线"
selectedBgView.isHidden = !isSelected
}
private func setupSubviews() {
contentView.addSubview(bgView)
bgView.addSubview(selectedBgView)
bgView.addSubview(dateLab)
bgView.addSubview(detailView)
bgView.layoutChain
.top()
.edgesHorzontal(15)
.height(137)
.bottom(12)
selectedBgView.layoutChain.edges()
dateLab.layoutChain
.top(9)
.left(15)
detailView.layoutChain
.topToBottomOfView(dateLab, offset: 8)
.edges(all: 15, excludingEdge: .top)
}
lazy var bgView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#EEFAFF")
view.cornerRadius = 10
return view
}()
lazy var selectedBgView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#C0EAFF")
view.borderWidth = 1
view.borderColor = UIColor(hexStr: "#16B3FF")
view.cornerRadius = 10
view.isHidden = true
return view
}()
lazy var dateLab: UILabel = {
let label = UILabel()
label.text = " "
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#0F2846")
return label
}()
lazy var detailView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 10
view.addSubview(iconView)
iconView.layoutChain
.left(15)
.centerY()
.width(50)
.heightToWidth(1)
view.addSubview(nameLab)
nameLab.layoutChain
.leftToRightOfView(iconView, offset: 14)
.right(15, relation: .greaterThanOrEqual)
.centerY(iconView)
return view
}()
lazy var iconView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
return view
}()
lazy var nameLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#0F2846")
return label
}()
override init(style: CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
setupSubviews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -182,25 +182,8 @@ class HomeView: UIView {
toolsView.layoutChain
.left(15)
.bottom(kScreenHeight / 2 - 58)
.width(40)
bubbleView.layoutChain.top(15).height(58)
signInView.layoutChain.height(58)
sosView.layoutChain
.height(56)
sosIcon.layoutChain
.top()
.centerX()
.width(28)
.height(28)
sosLab.layoutChain
.topToBottomOfView(sosIcon, offset: 4)
.edgesHorzontal()
.centerY(self, offset: -50)
searchLottieView.layoutChain
.centerY(toolsView)
@ -209,7 +192,6 @@ class HomeView: UIView {
.height(100)
locationView.layoutChain
.topToBottomOfView(searchLottieView, offset: 8)
.right(15)
.bottomToView(toolsView)
.width(40)
@ -647,10 +629,10 @@ class HomeView: UIView {
// MARK: -
lazy var toolsView: UIStackView = {
let view = UIStackView(arrangedSubviews: [bubbleView, signInView, sosView])
let view = UIStackView(arrangedSubviews: [bubbleView, signInView, sosView, scheduleView])
view.axis = .vertical
view.distribution = .equalSpacing
view.alignment = .center
view.distribution = .fill
view.alignment = .fill
view.backgroundColor = .black.withAlphaComponent(0.5)
view.cornerRadius = 20
return view
@ -663,7 +645,7 @@ class HomeView: UIView {
view.addSubview(bubbleIcon)
bubbleIcon.layoutChain
.top()
.top(10)
.centerX()
.width(28)
.height(28)
@ -681,6 +663,7 @@ class HomeView: UIView {
.height(2)
.centerX()
.bottom(7)
lineView.layoutChain.topToBottomOfView(bubbleLab, offset: 4)
return view
}()
@ -727,6 +710,7 @@ class HomeView: UIView {
.height(2)
.centerX()
.bottom(7)
lineView.layoutChain.topToBottomOfView(signInLab, offset: 4)
return view
}()
@ -753,6 +737,27 @@ class HomeView: UIView {
view.backgroundColor = .clear
view.addSubview(sosIcon)
view.addSubview(sosLab)
sosIcon.layoutChain
.top()
.centerX()
.width(28)
.height(28)
sosLab.layoutChain
.topToBottomOfView(sosIcon, offset: 4)
.edgesHorzontal()
let lineView = UIView()
lineView.backgroundColor = .white
view.addSubview(lineView)
lineView.layoutChain
.width(12)
.height(2)
.centerX()
.bottom(7)
lineView.layoutChain.topToBottomOfView(sosLab, offset: 4)
return view
}()
@ -773,6 +778,34 @@ class HomeView: UIView {
return label
}()
//
lazy var scheduleView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let icon = UIImageView(image: UIImage(named: "Home/schedule"))
icon.contentMode = .scaleAspectFill
view.addSubview(icon)
icon.layoutChain
.top()
.centerX()
.width(28)
.height(28)
let label = UILabel()
label.text = "行程"
label.font = .systemFont(ofSize: 10, weight: .medium)
label.textColor = .white
label.textAlignment = .center
view.addSubview(label)
label.layoutChain
.topToBottomOfView(icon, offset: 4)
.edgesHorzontal()
label.layoutChain.bottom(10)
return view
}()
// MARK: -
lazy var searchLottieView: LottieAnimationView = {
let view = LottieAnimationView(name: "home_search")

View File

@ -87,7 +87,7 @@ class HomeViewController: BaseViewController {
private func startLocationTimer() {
locationTimer?.invalidate()
locationTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in
locationTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { [weak self] _ in
guard let self = self, let loc = self.lastLocation else { return }
let coord = loc.coordinate
@ -146,6 +146,12 @@ class HomeViewController: BaseViewController {
AppRouter.push(vc)
}.disposed(by: disposeBag)
//
rootView.scheduleView.rx.tapGesture.subscribe { _ in
guard let model = self.viewModel.groupModel else { return }
AppRouter.push(Route.groupSchedule, userInfo: ["groupKey": model.default_group_key])
}.disposed(by: disposeBag)
//
rootView.groupView.rx.tapGesture.subscribe { _ in
guard let groupModel = self.viewModel.groupModel else { return }

View File

@ -88,9 +88,11 @@ class HomeViewModel {
var memberList: [GroupMemberModel] = [] {
didSet {
// -> -> 线
var tempList = memberList
tempList.moveToFirst { $0.user_id == AppContextManager.shared.userId }
tempList.moveToFirst { isGroupOwn(id: $0.user_id) }
tempList.moveToFirst { $0.is_online == true } // 线
tempList.moveToFirst { $0.user_id == AppContextManager.shared.userId } //
tempList.moveToFirst { isGroupOwn(id: $0.user_id) } //
memberList = tempList
sectionedItems.onNext(memberList.mapSection())
}

View File

@ -0,0 +1,240 @@
//
// GroupChooseView.swift
// QuickLocation
//
// Created by on 2026/6/27.
//
import UIKit
import ObjectMapper
class GroupChooseView: UIView {
private static let shared = GroupChooseView(frame: CGRect(origin: .zero, size: kScreenSize))
private var groupModel: GroupModel? {
didSet {
guard let model = groupModel else { return }
groupList = model.groups
let count = min(model.groups.count, 5)
tableView.isScrollEnabled = model.groups.count > 5
tableView.layoutChain.height(CGFloat(count * 68), relation: .greaterThanOrEqual)
}
}
private var groupInfo: [String: Any] {
guard let model = groupModel,
let groupInfoModel = model.groups.first(where: { $0.group_key == defaultGroupKey }) else { return [:] }
return groupInfoModel.toJSON()
}
private var defaultGroupKey: String = "" {
didSet { tableView.reloadData() }
}
private var groupList: [GroupInfoModel] = [] {
didSet { tableView.reloadData() }
}
private var completion: ((String?) -> Void)?
@objc func tap() { completion?(nil) }
@objc func cancelAction(button: UIButton) { completion?(nil) }
@objc func inviteAction(button: UIButton) {
completion?(nil)
AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": self.groupInfo])
}
private lazy var bgView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.5)
view.clipsToBounds = true
return view
}()
lazy var infoView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 10
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
return view
}()
lazy var titleLab: UILabel = {
let label = UILabel()
label.text = "选择圈子"
label.font = .systemFont(ofSize: 16, weight: .bold)
label.textColor = ThemeManager.shared.color.titleAuxColor
return label
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
tableView.backgroundColor = .white
tableView.separatorStyle = .none
tableView.estimatedRowHeight = 68
tableView.showsVerticalScrollIndicator = false
tableView.bounces = false
tableView.isScrollEnabled = false
tableView.register(GroupListCell.self)
tableView.tableHeaderView = UIView(frame: CGRectMake(0, 0, kScreenWidth, 10))
tableView.dataSource = self
tableView.delegate = self
return tableView
}()
lazy var cancelBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("取消", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 15, weight: .medium)
btn.backgroundColor = .white
btn.borderWidth = 1
btn.borderColor = UIColor(hexStr: "#16B3FF")
btn.cornerRadius = 20
btn.addTarget(self, action: #selector(cancelAction), for: .touchUpInside)
return btn
}()
lazy var inviteBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("邀请", for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 20
btn.addTarget(self, action: #selector(inviteAction), for: .touchUpInside)
return btn
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
addSubview(bgView)
bgView.addSubview(infoView)
infoView.addSubview(titleLab)
infoView.addSubview(tableView)
infoView.addSubview(cancelBtn)
infoView.addSubview(inviteBtn)
// bgView
bgView.layoutChain.edges()
// infoView
infoView.layoutChain
.edgesHorzontal()
.bottom()
titleLab.layoutChain
.top(16)
.left(12)
cancelBtn.layoutChain
.left(15)
.bottom(20 + kSafeBottomMargin)
.widthToView(inviteBtn)
.height(44)
inviteBtn.layoutChain
.leftToRightOfView(cancelBtn, offset: 7)
.right(15)
.bottomToView(cancelBtn)
.height(44)
.widthToView(inviteBtn)
tableView.layoutChain
.topToBottomOfView(titleLab, offset: 10)
.edgesHorzontal()
.bottomToTopOfView(cancelBtn, offset: -17)
.height(78, relation: .greaterThanOrEqual)
let tap = UITapGestureRecognizer(target: self, action: #selector(tap))
tap.delegate = self
addGestureRecognizer(tap)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
}
}
// MARK: - Public
extension GroupChooseView {
///
static func show(groupModel: GroupModel,
completion: @escaping ((String?) -> Void)) {
guard let superView = kKeyWindow else { return }
let shared = GroupChooseView.shared
if shared.superview != nil {
shared.removeFromSuperview()
}
shared.groupModel = groupModel
shared.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
superView.addSubview(shared)
superView.bringSubviewToFront(shared)
shared.bgView.alpha = 0
shared.infoView.transform = CGAffineTransform(translationX: 0, y: shared.infoView.frame.maxY + 100)
shared.completion = { text in
completion(text)
GroupChooseView.dismiss()
}
shared.layoutIfNeeded()
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
shared.bgView.alpha = 1
shared.infoView.transform = .identity
}
}
///
static func dismiss() {
guard GroupChooseView.shared.superview != nil else { return }
let shared = GroupChooseView.shared
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn) {
shared.bgView.alpha = 0
shared.infoView.transform = CGAffineTransform(translationX: 0, y: shared.infoView.frame.maxY + 100)
} completion: { _ in
shared.removeFromSuperview()
shared.infoView.transform = .identity
}
}
}
// MARK: - UIGestureRecognizerDelegate
extension GroupChooseView: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if let view = touch.view, !(view == self || view == bgView) {
return false
}
return true
}
}
// MARK: - UITableViewDataSource & UITableViewDelegate
extension GroupChooseView: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
groupList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: GroupListCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model: groupList[indexPath.row],
isSelected: defaultGroupKey == groupList[indexPath.row].group_key)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
defaultGroupKey = groupList[indexPath.row].group_key
}
}

View File

@ -22,6 +22,7 @@ class SearchLocationResultVC: BaseViewController {
private let phone: String
private let code: Int
private let memberData: [String : Any]
private var groupModel: GroupModel?
override func viewDidLoad() {
super.viewDidLoad()
@ -41,8 +42,12 @@ class SearchLocationResultVC: BaseViewController {
userInfo: self.memberData)
AppRouter.shared.popToRoot()
}
else { // Ta
else { // Ta
guard let model = self.groupModel else {
self.requestGroupInfo(showPicker: true)
return
}
GroupChooseView.show(groupModel: model) { _ in }
}
}).disposed(by: disposeBag)
}
@ -103,10 +108,13 @@ class SearchLocationResultVC: BaseViewController {
}
// MARK: -
private func requestGroupInfo() {
GroupService.groupInfo().subscribe { response in
guard let model = response.model else { return }
private func requestGroupInfo(showPicker: Bool = false) {
GroupService.groupInfo().subscribe { [weak self] response in
guard let self = self, let model = response.model else { return }
self.groupModel = model
if showPicker {
GroupChooseView.show(groupModel: model) { _ in }
}
}.disposed(by: disposeBag)
}

View File

@ -61,8 +61,4 @@ final class LoginViewModel: BaseViewModel {
func performAppleLogin() {
// TODO: Integrate Sign in with Apple
}
func performQQLogin() {
// TODO: Integrate QQ SDK
}
}

View File

@ -43,6 +43,24 @@ struct ScheduleListResponse: BaseModelProtocol, ListModelType {
}
}
///
struct groupScheduleListResponse: BaseModelProtocol {
//
var code: String?
//
var message: String?
//
var list: [ScheduleModel] = []
init?(map: Map) {}
mutating func mapping(map: Map) {
code <- map["code"]
message <- map["msg"]
list <- map["data"]
}
}
struct ScheduleModel: Mappable, Equatable {
var uuid: String = UUID().uuidString
/// id

View File

@ -30,6 +30,18 @@ struct ItineraryService {
}
}
///
static func groupScheduleList(groupKey: String) -> Observable<groupScheduleListResponse> {
let api = ItineraryAPI.query(follow: false,
own: false,
history: false,
group_key: groupKey,
page: -1).multiTarget
return APIProvider.request(token: api)
.map(groupScheduleListResponse.self)
.asObservable()
}
///
/// - Parameters:
/// - id: ID