diff --git a/QuickLocation.xcodeproj/project.pbxproj b/QuickLocation.xcodeproj/project.pbxproj index 10aa486..b8d1d35 100644 --- a/QuickLocation.xcodeproj/project.pbxproj +++ b/QuickLocation.xcodeproj/project.pbxproj @@ -182,6 +182,9 @@ 30BAB8532FCD337C00C33B5C /* GroupService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8522FCD337C00C33B5C /* GroupService.swift */; }; 30BAB8632FCD716C00C33B5C /* JoinGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8622FCD716C00C33B5C /* JoinGroupVC.swift */; }; 30BAB8652FCD718A00C33B5C /* JoinGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8642FCD718A00C33B5C /* JoinGroupView.swift */; }; + 30BF300C2FED09BA00D9CB52 /* ScheduleDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BF300B2FED09BA00D9CB52 /* ScheduleDetailView.swift */; }; + 30BF300E2FED09CC00D9CB52 /* ScheduleDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BF300D2FED09CC00D9CB52 /* ScheduleDetailVC.swift */; }; + 30BF30102FED0C8E00D9CB52 /* ScheduleDetailVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BF300F2FED0C8E00D9CB52 /* ScheduleDetailVM.swift */; }; 30C4C0162FDB91B8009215C1 /* CheckPermissionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C0152FDB91B8009215C1 /* CheckPermissionVC.swift */; }; 30C4C0192FDBF094009215C1 /* RemoveMemberVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C0182FDBF094009215C1 /* RemoveMemberVC.swift */; }; 30C4C01B2FDBF09D009215C1 /* RemoveMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C01A2FDBF09D009215C1 /* RemoveMemberView.swift */; }; @@ -216,6 +219,7 @@ 30D87D052FE1336300E958FD /* NavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87D022FE1336300E958FD /* NavigationView.swift */; }; 30D891F52FE22E0600E958FD /* OrderAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D891F42FE22E0600E958FD /* OrderAPI.swift */; }; 30D891F72FE22E6E00E958FD /* OrderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D891F62FE22E6E00E958FD /* OrderService.swift */; }; + 30DA36BD2FECC5AB008D5A2C /* CreateScheduleVipPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DA36BC2FECC5AB008D5A2C /* CreateScheduleVipPopView.swift */; }; 30DC18522FD009CD0041DCD1 /* VipExpenseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18512FD009CD0041DCD1 /* VipExpenseModel.swift */; }; 30DC18542FD00C4A0041DCD1 /* VipRechargeVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18532FD00C4A0041DCD1 /* VipRechargeVM.swift */; }; 30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18562FD11E7A0041DCD1 /* WebOperations.swift */; }; @@ -441,6 +445,9 @@ 30BAB8522FCD337C00C33B5C /* GroupService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupService.swift; sourceTree = ""; }; 30BAB8622FCD716C00C33B5C /* JoinGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinGroupVC.swift; sourceTree = ""; }; 30BAB8642FCD718A00C33B5C /* JoinGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinGroupView.swift; sourceTree = ""; }; + 30BF300B2FED09BA00D9CB52 /* ScheduleDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleDetailView.swift; sourceTree = ""; }; + 30BF300D2FED09CC00D9CB52 /* ScheduleDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleDetailVC.swift; sourceTree = ""; }; + 30BF300F2FED0C8E00D9CB52 /* ScheduleDetailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleDetailVM.swift; sourceTree = ""; }; 30C4C0112FDABC8C009215C1 /* QuickLocation.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QuickLocation.entitlements; sourceTree = ""; }; 30C4C0152FDB91B8009215C1 /* CheckPermissionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckPermissionVC.swift; sourceTree = ""; }; 30C4C0182FDBF094009215C1 /* RemoveMemberVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveMemberVC.swift; sourceTree = ""; }; @@ -476,6 +483,7 @@ 30D87D022FE1336300E958FD /* NavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationView.swift; sourceTree = ""; }; 30D891F42FE22E0600E958FD /* OrderAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderAPI.swift; sourceTree = ""; }; 30D891F62FE22E6E00E958FD /* OrderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderService.swift; sourceTree = ""; }; + 30DA36BC2FECC5AB008D5A2C /* CreateScheduleVipPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleVipPopView.swift; sourceTree = ""; }; 30DC18512FD009CD0041DCD1 /* VipExpenseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipExpenseModel.swift; sourceTree = ""; }; 30DC18532FD00C4A0041DCD1 /* VipRechargeVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipRechargeVM.swift; sourceTree = ""; }; 30DC18552FD11E7A0041DCD1 /* NavigationTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTitleView.swift; sourceTree = ""; }; @@ -1226,6 +1234,16 @@ path = Join; sourceTree = ""; }; + 30BF300A2FED09A300D9CB52 /* ScheduleDetail */ = { + isa = PBXGroup; + children = ( + 30BF300D2FED09CC00D9CB52 /* ScheduleDetailVC.swift */, + 30BF300F2FED0C8E00D9CB52 /* ScheduleDetailVM.swift */, + 30BF300B2FED09BA00D9CB52 /* ScheduleDetailView.swift */, + ); + path = ScheduleDetail; + sourceTree = ""; + }; 30C4C0122FDB9178009215C1 /* CheckPermission */ = { isa = PBXGroup; children = ( @@ -1283,6 +1301,7 @@ 30D74AB92FEA37AD0050EB2C /* ScheduleModel.swift */, 30D74ABB2FEA67CE0050EB2C /* CreateSchedule */, 30D74BF22FEB6F5B0050EB2C /* LocationPicker */, + 30BF300A2FED09A300D9CB52 /* ScheduleDetail */, ); path = Schedule; sourceTree = ""; @@ -1294,6 +1313,7 @@ 30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */, 30D74ABE2FEA67F30050EB2C /* CreateScheduleView.swift */, 30D74AC02FEA6EEF0050EB2C /* CreateSchedulePopView.swift */, + 30DA36BC2FECC5AB008D5A2C /* CreateScheduleVipPopView.swift */, ); path = CreateSchedule; sourceTree = ""; @@ -1698,6 +1718,7 @@ 305A76C02FCA8C7000227D26 /* BaseNavigationController.swift in Sources */, 305A76C12FCA8C7000227D26 /* BaseViewController.swift in Sources */, 3062E8C02FCED7BB00CEF511 /* GroupIconListView.swift in Sources */, + 30BF300C2FED09BA00D9CB52 /* ScheduleDetailView.swift in Sources */, 305A76C22FCA8C7000227D26 /* BaseViewModel.swift in Sources */, 30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */, 30DC185B2FD11E7A0041DCD1 /* NavigationTitleView.swift in Sources */, @@ -1799,6 +1820,7 @@ 305A76F92FCA8C7000227D26 /* DLToast.swift in Sources */, 305A76FA2FCA8C7000227D26 /* DLEmptyDataSet.swift in Sources */, 305A76FB2FCA8C7000227D26 /* EmptyDataSet.swift in Sources */, + 30BF300E2FED09CC00D9CB52 /* ScheduleDetailVC.swift in Sources */, 305A76FC2FCA8C7000227D26 /* EmptyDataSetDelegate.swift in Sources */, 305A76FD2FCA8C7000227D26 /* EmptyDataSetSource.swift in Sources */, 30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */, @@ -1816,6 +1838,8 @@ 30BAB8652FCD718A00C33B5C /* JoinGroupView.swift in Sources */, 305A77062FCA8C7000227D26 /* MXScrollViewController.m in Sources */, 305A77072FCA8C7000227D26 /* Helper.swift in Sources */, + 30BF30102FED0C8E00D9CB52 /* ScheduleDetailVM.swift in Sources */, + 30DA36BD2FECC5AB008D5A2C /* CreateScheduleVipPopView.swift in Sources */, 305A77082FCA8C7000227D26 /* PageCollectionViewFlowLayout.swift in Sources */, 3062E8C42FCFC90F00CEF511 /* CreateGroupVipPopView.swift in Sources */, 30D74ABA2FEA37AD0050EB2C /* ScheduleModel.swift in Sources */, diff --git a/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate b/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate index 3650560..17d18b1 100644 Binary files a/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate and b/QuickLocation.xcworkspace/xcuserdata/yanghong.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/QuickLocation/API/ItineraryAPI.swift b/QuickLocation/API/ItineraryAPI.swift index 79fa789..e192c30 100644 --- a/QuickLocation/API/ItineraryAPI.swift +++ b/QuickLocation/API/ItineraryAPI.swift @@ -11,7 +11,7 @@ internal import Alamofire /// 行程API enum ItineraryAPI { - /// 查询行程 + /// 查询行程列表 /// - Parameters: /// - follow: 只查看关注的行程 /// - own: 只查看自己创建的行程 @@ -19,6 +19,23 @@ enum ItineraryAPI { /// - group_key: 过滤圈子查询 case query(follow: Bool, own: Bool, history: Bool, group_key: String, page: Int) + /// 查询行程关注人 + /// - Parameters: + /// - id: 行程ID + case queryFollowList(id: String) + + /// 设置行程 + /// - Parameters: + /// - id: 更新时传入 + /// - group_keys: 只查看关注的行程 + /// - timestamp: 行程日期 + /// - points: 行程点 + case set(id: String, group_keys: [String], timestamp: Int64, points: [[String: Any]]) + + /// 删除 + /// - Parameters: + /// - id: 行程ID + case delete(id: String) } extension ItineraryAPI: MultiTargetProtocol { @@ -27,13 +44,21 @@ extension ItineraryAPI: MultiTargetProtocol { switch self { case .query: return "mapi/itinerary/route" + case .queryFollowList: + return "mapi/itinerary/route/follower" + case .set: + return "mapi/itinerary/route/set" + case .delete: + return "mapi/itinerary/route/delete" } } var method: Moya.Method { switch self { - case .query: + case .query, .queryFollowList: return .get + case .delete: + return .delete default: return .post } @@ -50,6 +75,29 @@ extension ItineraryAPI: MultiTargetProtocol { params["page"] = page params["limit"] = 20 return .requestParameters(parameters: params, encoding: URLEncoding()) + + case let .queryFollowList(id): + var params = Parameters() + params["route_id"] = id + return .requestParameters(parameters: params, encoding: URLEncoding()) + + case let .set(id, group_keys, timestamp, points): + var params = Parameters() + if id.isEmpty { + params["timestamp"] = timestamp + } + else { + params["id"] = id + } + params["group_keys"] = group_keys + params["points"] = points + + return .requestParameters(parameters: params, encoding: JSONEncoding()) + + case let .delete(id): + var params = Parameters() + params["id"] = id + return .requestParameters(parameters: params, encoding: URLEncoding()) } } } diff --git a/QuickLocation/Assets.xcassets/Common/delete.imageset/Contents.json b/QuickLocation/Assets.xcassets/Common/delete.imageset/Contents.json new file mode 100644 index 0000000..3b7517d --- /dev/null +++ b/QuickLocation/Assets.xcassets/Common/delete.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_2302@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_2302@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/Common/delete.imageset/Group_2302@2x.png b/QuickLocation/Assets.xcassets/Common/delete.imageset/Group_2302@2x.png new file mode 100644 index 0000000..9f60c3b Binary files /dev/null and b/QuickLocation/Assets.xcassets/Common/delete.imageset/Group_2302@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Common/delete.imageset/Group_2302@3x.png b/QuickLocation/Assets.xcassets/Common/delete.imageset/Group_2302@3x.png new file mode 100644 index 0000000..4effca6 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Common/delete.imageset/Group_2302@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Schedule/forward.imageset/Contents.json b/QuickLocation/Assets.xcassets/Schedule/forward.imageset/Contents.json new file mode 100644 index 0000000..8a7f46c --- /dev/null +++ b/QuickLocation/Assets.xcassets/Schedule/forward.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_2381@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_2381@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/Schedule/forward.imageset/Group_2381@2x.png b/QuickLocation/Assets.xcassets/Schedule/forward.imageset/Group_2381@2x.png new file mode 100644 index 0000000..1d5502c Binary files /dev/null and b/QuickLocation/Assets.xcassets/Schedule/forward.imageset/Group_2381@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Schedule/forward.imageset/Group_2381@3x.png b/QuickLocation/Assets.xcassets/Schedule/forward.imageset/Group_2381@3x.png new file mode 100644 index 0000000..fbc407d Binary files /dev/null and b/QuickLocation/Assets.xcassets/Schedule/forward.imageset/Group_2381@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Schedule/route.imageset/Contents.json b/QuickLocation/Assets.xcassets/Schedule/route.imageset/Contents.json new file mode 100644 index 0000000..1693392 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Schedule/route.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_2385@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_2385@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/Schedule/route.imageset/Group_2385@2x.png b/QuickLocation/Assets.xcassets/Schedule/route.imageset/Group_2385@2x.png new file mode 100644 index 0000000..be40834 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Schedule/route.imageset/Group_2385@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Schedule/route.imageset/Group_2385@3x.png b/QuickLocation/Assets.xcassets/Schedule/route.imageset/Group_2385@3x.png new file mode 100644 index 0000000..f9428d9 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Schedule/route.imageset/Group_2385@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Schedule/upgrade_bg.imageset/Contents.json b/QuickLocation/Assets.xcassets/Schedule/upgrade_bg.imageset/Contents.json new file mode 100644 index 0000000..379ba42 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Schedule/upgrade_bg.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "按钮@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "按钮@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/Schedule/upgrade_bg.imageset/按钮@2x.png b/QuickLocation/Assets.xcassets/Schedule/upgrade_bg.imageset/按钮@2x.png new file mode 100644 index 0000000..4a45e76 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Schedule/upgrade_bg.imageset/按钮@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Schedule/upgrade_bg.imageset/按钮@3x.png b/QuickLocation/Assets.xcassets/Schedule/upgrade_bg.imageset/按钮@3x.png new file mode 100644 index 0000000..aeb690e Binary files /dev/null and b/QuickLocation/Assets.xcassets/Schedule/upgrade_bg.imageset/按钮@3x.png differ diff --git a/QuickLocation/Assets.xcassets/Schedule/vip_pop.imageset/Contents.json b/QuickLocation/Assets.xcassets/Schedule/vip_pop.imageset/Contents.json new file mode 100644 index 0000000..9236155 --- /dev/null +++ b/QuickLocation/Assets.xcassets/Schedule/vip_pop.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group_2396@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group_2396@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickLocation/Assets.xcassets/Schedule/vip_pop.imageset/Group_2396@2x.png b/QuickLocation/Assets.xcassets/Schedule/vip_pop.imageset/Group_2396@2x.png new file mode 100644 index 0000000..8dbea1c Binary files /dev/null and b/QuickLocation/Assets.xcassets/Schedule/vip_pop.imageset/Group_2396@2x.png differ diff --git a/QuickLocation/Assets.xcassets/Schedule/vip_pop.imageset/Group_2396@3x.png b/QuickLocation/Assets.xcassets/Schedule/vip_pop.imageset/Group_2396@3x.png new file mode 100644 index 0000000..4586031 Binary files /dev/null and b/QuickLocation/Assets.xcassets/Schedule/vip_pop.imageset/Group_2396@3x.png differ diff --git a/QuickLocation/Manager/App/RouterManager.swift b/QuickLocation/Manager/App/RouterManager.swift index 7e66907..2b31ced 100644 --- a/QuickLocation/Manager/App/RouterManager.swift +++ b/QuickLocation/Manager/App/RouterManager.swift @@ -55,6 +55,8 @@ enum Route: String { case checkPermission = "checkPermission" /// 创建行程 case createSchedule = "createSchedule" + /// 行程详情 + case scheduleDetail = "scheduleDetail" } extension Route: RouterTarget { @@ -262,7 +264,16 @@ extension AppRouter: AppRouterProtocol { // MARK: - 创建行程 AppRouter.register(Route.createSchedule) { url, parameters in - CreateScheduleVC() + let routeId = parameters["routeId"].safeString + return CreateScheduleVC(routeId: routeId, + scheduleJson: parameters["scheduleJson"].safeDictionary as! [String : Any]) + } + + // MARK: - 行程详情 + AppRouter.register(Route.scheduleDetail) { url, parameters in + let routeId = parameters["routeId"].safeString + return ScheduleDetailVC(routeId: routeId, + scheduleJson: parameters["scheduleJson"].safeDictionary as! [String : Any]) } } } diff --git a/QuickLocation/Section/Schedule/CreateSchedule/CreateSchedulePopView.swift b/QuickLocation/Section/Schedule/CreateSchedule/CreateSchedulePopView.swift index a925f1f..2a67e88 100644 --- a/QuickLocation/Section/Schedule/CreateSchedule/CreateSchedulePopView.swift +++ b/QuickLocation/Section/Schedule/CreateSchedule/CreateSchedulePopView.swift @@ -16,9 +16,9 @@ import TagListView // MARK: - CreateSchedulePopView class CreateSchedulePopView: UIView { - + var disposeBag = DisposeBag() - + func setupTagData(_ list: [GroupInfoModel]) { let nameArr = list.map { $0.name } tagListView.removeAllTags() @@ -35,18 +35,18 @@ class CreateSchedulePopView: UIView { addSubview(dateView) addSubview(detailView) addSubview(scrollView) - + lineView.layoutChain .top(15) .centerX() .width(36) .height(4) - + dateView.layoutChain .topToBottomOfView(lineView, offset: 19) .edgesHorzontal() .height(22) - + detailView.layoutChain .topToBottomOfView(dateView, offset: 20) .edgesHorzontal() @@ -56,16 +56,16 @@ class CreateSchedulePopView: UIView { .topToBottomOfView(detailView, offset: 15) .edges(excludingEdge: .top) } - + // MARK: - Views - + lazy var lineView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#EBEBEB") v.cornerRadius = 2 return v }() - + lazy var dateView: UIView = { let view = UIView() view.backgroundColor = .clear @@ -79,7 +79,7 @@ class CreateSchedulePopView: UIView { dateLab.layoutChain.leftToRightOfView(titleLab).centerY() return view }() - + lazy var dateLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14, weight: .medium) @@ -90,7 +90,7 @@ class CreateSchedulePopView: UIView { label.text = fmt.string(from: Date()) return label }() - + lazy var detailView: UIView = { let view = UIView() view.backgroundColor = .clear @@ -104,7 +104,7 @@ class CreateSchedulePopView: UIView { addBtn.layoutChain.right(15).centerY().width(18).height(18) return view }() - + lazy var addBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Schedule/add"), for: .normal) @@ -112,7 +112,7 @@ class CreateSchedulePopView: UIView { btn.extendEdgeInsets = UIEdgeInsets(top: 30, left: 30, bottom: 10, right: 15) return btn }() - + lazy var scrollView: UIScrollView = { let view = UIScrollView() view.backgroundColor = .white @@ -145,7 +145,7 @@ class CreateSchedulePopView: UIView { return view }() - + lazy var tableView: UITableView = { let tv = UITableView(frame: .zero, style: .plain) tv.backgroundColor = .clear @@ -157,7 +157,7 @@ class CreateSchedulePopView: UIView { tv.register(SchedulePointCell.self) return tv }() - + /// 选择分享的圈子 lazy var shareGroupView: UIView = { let view = UIView() @@ -207,22 +207,19 @@ class CreateSchedulePopView: UIView { override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .white + // 使用原生圆角避免 CAShapeLayer mask 隐式动画导致的弹跳 + layer.cornerRadius = 30 + layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + layer.masksToBounds = true setupUI() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - override func layoutSubviews() { - super.layoutSubviews() - layoutIfNeeded() - setCornerRadius(corners: [.topLeft, .topRight], withCornerRadii: CGSize(width: 30, height: 30)) - } } - // MARK: - SchedulePointCell class SchedulePointCell: UITableViewCell { @@ -231,11 +228,12 @@ class SchedulePointCell: UITableViewCell { var onLocationTap: (() -> Void)? var onTimeTap: (() -> Void)? + var onRemarkChanged: ((String) -> Void)? func configure(item: SchedulePointItem, index: Int, total: Int, onDelete: @escaping () -> Void) { - disposeBag = DisposeBag() indexLabel.text = "\(index + 1)" - locationLabel.text = item.locationName.isEmpty ? "点击选择地点" : item.locationName + locationLabel.text = item.street.isEmpty ? "点击选择地点" : item.street + remarkTF.text = item.remark locationLabel.rx.tapGesture .when(.recognized) @@ -274,6 +272,10 @@ class SchedulePointCell: UITableViewCell { // MARK: - Init + @objc private func remarkDidChange() { + onRemarkChanged?(remarkTF.text ?? "") + } + override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none @@ -283,6 +285,11 @@ class SchedulePointCell: UITableViewCell { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override func prepareForReuse() { + super.prepareForReuse() + disposeBag = DisposeBag() + } + private func setupViews() { contentView.addSubview(topDashView) contentView.addSubview(pointIcon) @@ -297,7 +304,7 @@ class SchedulePointCell: UITableViewCell { cardCornerView.addSubview(deleteBtn) topDashView.layoutChain - .top().width(2) + .top().width(1) pointIcon.layoutChain .centerY() @@ -311,7 +318,7 @@ class SchedulePointCell: UITableViewCell { bottomDashView.layoutChain .topToBottomOfView(pointIcon, offset: 10) .centerX(pointIcon) - .width(2) + .width(1) .bottom() // 右侧卡片 @@ -349,15 +356,15 @@ class SchedulePointCell: UITableViewCell { .leftToView(locationLabel) .rightToLeftOfView(deleteBtn, offset: -10) .bottom(15) - + deleteBtn.layoutChain.centerY(remarkTF) } // MARK: - Views - private let topDashView: UIView = { - let v = UIView() - v.backgroundColor = UIColor(hexStr: "#E0E0E0") + private let topDashView: DashLineView = { + let v = DashLineView() v.isHidden = true + v.backgroundColor = .clear return v }() @@ -367,10 +374,10 @@ class SchedulePointCell: UITableViewCell { return iv }() - private let bottomDashView: UIView = { - let v = UIView() - v.backgroundColor = UIColor(hexStr: "#E0E0E0") + private let bottomDashView: DashLineView = { + let v = DashLineView() v.isHidden = true + v.backgroundColor = .clear return v }() @@ -438,3 +445,45 @@ class SchedulePointCell: UITableViewCell { indexLabel.setCornerRadius(corners: [.bottomRight], withCornerRadii: CGSize(width: 10, height: 10)) } } + +/// 虚线绘制视图(竖线) +class DashLineView: UIView { + override func layoutSubviews() { + super.layoutSubviews() + layer.sublayers?.forEach { if $0 is CAShapeLayer { $0.removeFromSuperlayer() } } + + let shapeLayer = CAShapeLayer() + shapeLayer.strokeColor = UIColor(hexStr: "#E0E0E0").cgColor + shapeLayer.lineWidth = 1 + shapeLayer.lineDashPattern = [4, 4] + shapeLayer.fillColor = nil + + let path = UIBezierPath() + path.move(to: CGPoint(x: bounds.midX, y: 0)) + path.addLine(to: CGPoint(x: bounds.midX, y: bounds.height)) + shapeLayer.path = path.cgPath + + layer.addSublayer(shapeLayer) + } +} + +/// 虚线绘制视图(横线) +class HorizontalDashLineView: UIView { + override func layoutSubviews() { + super.layoutSubviews() + layer.sublayers?.forEach { if $0 is CAShapeLayer { $0.removeFromSuperlayer() } } + + let shapeLayer = CAShapeLayer() + shapeLayer.strokeColor = UIColor(hexStr: "#E0E0E0").cgColor + shapeLayer.lineWidth = 1 + shapeLayer.lineDashPattern = [4, 4] + shapeLayer.fillColor = nil + + let path = UIBezierPath() + path.move(to: CGPoint(x: 0, y: bounds.midY)) + path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.midY)) + shapeLayer.path = path.cgPath + + layer.addSublayer(shapeLayer) + } +} diff --git a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift index e1c0fac..0545f5c 100644 --- a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift +++ b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift @@ -17,6 +17,7 @@ import RxGesture import AMapNaviKit import AMapSearchKit import TagListView +import ObjectMapper #endif class CreateScheduleVC: BaseViewController, MAMapViewDelegate { @@ -24,7 +25,7 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { override var isNavigationBarHidden: Bool { true } fileprivate var rootView: CreateScheduleView! - private let viewModel = CreateScheduleVM() + private let viewModel: CreateScheduleVM private var popView: CreateSchedulePopView { rootView.createSchedulePopView } override func loadView() { @@ -39,8 +40,13 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { popView.tagListView.delegate = self setupMap() bindViewModel() - + reactiveAction() requestGroupInfo() + + guard let _ = viewModel.scheduModel else { return } + rootView.navTitleLabel.text = "编辑行程" + rootView.deleteBtn.isHidden = false +// requestFollowList(id: model.id) } override func viewDidDisappear(_ animated: Bool) { @@ -52,12 +58,74 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { } } + // MARK: - Actions + private func reactiveAction() { + rootView.deleteBtn.rx.tap.subscribe(onNext: { _ in + guard let model = self.viewModel.scheduModel else { return } + self.showConfirmPop(title: "确定要删除吗?", + message: "此条行程路线将被永久删除", + confirmText: "删除", + confirmBlock: { + self.requestDelete(id: model.id) + }, cancelText: "取消") + }).disposed(by: disposeBag) + } + // MARK: - API + private func requestFollowList(id: String) { + dl.showLoading() + ItineraryService.queryFollowList(id: id).subscribe { response in + self.dl.dismiss() + }.disposed(by: disposeBag) + } + private func requestGroupInfo() { GroupService.groupInfo().subscribe { response in guard let model = response.model else { return } self.groupList = model.groups self.popView.setupTagData(model.groups) + // 编辑模式:预选中已分享的圈子 + #if !targetEnvironment(simulator) + if !self.viewModel.selectedGroupKeys.isEmpty { + for (i, group) in self.groupList.enumerated() { + guard i < self.popView.tagListView.tagViews.count, + self.viewModel.selectedGroupKeys.contains(group.group_key) else { continue } + self.popView.tagListView.tagViews[i].isSelected = true + } + } + #endif + }.disposed(by: disposeBag) + } + + private func requestSet(id: String="", points: [[String: Any]]) { + let selectedDate = Int64(viewModel.selectedDate.value.timeIntervalSince1970 * 1000) + dl.showLoading() + ItineraryService.set(id: id, + group_keys: viewModel.selectedGroupKeys, + timestamp: selectedDate, + points: points).subscribe(onNext: { response in + self.dl.dismiss() + self.dl.show(text: id.isEmpty ? "创建成功" : "更新成功") { + AppRouter.shared.popOrDismiss() + } + }, onError: { error in + guard let code = error.underlyingError?.code else { return } + if code == 20010 { // "创建的行程数达到上限" + CreateScheduleVipPopView.show() + } + else { + self.dl.show(text: error.localizedDescription) + } + }).disposed(by: disposeBag) + } + + private func requestDelete(id: String) { + dl.showLoading() + ItineraryService.delete(id: id).subscribe { response in + self.dl.dismiss() + self.dl.show(text: "删除成功") { + AppRouter.shared.popOrDismiss() + } }.disposed(by: disposeBag) } @@ -67,6 +135,14 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { RxTableViewSectionedReloadDataSource( configureCell: { [weak self] _, tv, indexPath, item in let cell: SchedulePointCell = tv.dequeueReusableCell(for: indexPath) + + cell.configure(item: item, index: indexPath.row, + total: self?.viewModel.pointsRelay.value.count ?? 0, + onDelete: { [weak self] in + self?.viewModel.deletePointAt.onNext(indexPath.row) + }) + + // 到达时间 cell.onTimeTap = { [weak self] in guard let self = self else { return } let picker = BRDatePickerView(pickerMode: .HM) @@ -92,7 +168,7 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { } picker.show() } - + // 地点 cell.onLocationTap = { [weak self] in guard let self = self else { return } let coord = item.latitude != 0 || item.longitude != 0 @@ -110,21 +186,34 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { ) } vc.onPickedLocation = { picked in - self.viewModel.updatePointLocation(index: indexPath.row, name: picked.name, address: picked.address) - // 补充坐标 var list = self.viewModel.pointsRelay.value guard indexPath.row < list.count else { return } + list[indexPath.row].locationName = picked.name + list[indexPath.row].address = picked.address list[indexPath.row].latitude = picked.coordinate.latitude list[indexPath.row].longitude = picked.coordinate.longitude + list[indexPath.row].province = picked.province + list[indexPath.row].city = picked.city + list[indexPath.row].district = picked.district + list[indexPath.row].street = picked.street + list[indexPath.row].country = picked.country + list[indexPath.row].formatted_address = picked.formatted_address self.viewModel.pointsRelay.accept(list) } self.present(vc, animated: true) } - cell.configure(item: item, index: indexPath.row, - total: self?.viewModel.pointsRelay.value.count ?? 0, - onDelete: { [weak self] in - self?.viewModel.deletePointAt.onNext(indexPath.row) - }) + + // 备注 + cell.remarkTF.rx.controlEvent(.editingDidEnd) + .subscribe(onNext: { + guard let self = self, let text = cell.remarkTF.text else { return } + var list = self.viewModel.pointsRelay.value + guard indexPath.row < list.count else { return } + list[indexPath.row].remark = text + self.viewModel.pointsRelay.accept(list) + }) + .disposed(by: cell.disposeBag) + return cell }) }() @@ -184,9 +273,6 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { guard hasTime.count == points.count else { DLToast.show(text: "请为每个行程点选择到达时间"); return } guard !viewModel.selectedGroupKeys.isEmpty else { DLToast.show(text: "请选择分享的圈子"); return } - // 时间戳(毫秒) - let ts = Int64(viewModel.selectedDate.value.timeIntervalSince1970 * 1000) - // 生成 points 数组 let pointsJSON: [[String: Any]] = points.map { p in let expectedTs = p.expectedTime.map { Int64($0.timeIntervalSince1970 * 1000) } ?? 0 @@ -205,13 +291,7 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { ] } - let params: [String: Any] = [ - "group_keys": viewModel.selectedGroupKeys, - "timestamp": ts, - "points": pointsJSON - ] - print("📋 Create schedule: \(params)") - DLToast.show(text: "创建成功") + requestSet(id: viewModel.scheduModel?.id ?? "", points: pointsJSON) } private func setupMap() { @@ -362,6 +442,17 @@ class CreateScheduleVC: BaseViewController, MAMapViewDelegate { } return nil } + + // MARK: - Init + init(routeId: String, scheduleJson: [String: Any]) { + let model = ScheduleModel.init(JSON: scheduleJson) + self.viewModel = CreateScheduleVM(scheduleJson.isEmpty ? nil : model) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } } // MARK: - TagListViewDelegate diff --git a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVM.swift b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVM.swift index 706f119..bf55108 100644 --- a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVM.swift +++ b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVM.swift @@ -32,6 +32,8 @@ class CreateScheduleVM { let disposeBag = DisposeBag() + let scheduModel: ScheduleModel? + // MARK: - Input let addPointTapped = PublishSubject() let deletePointAt = PublishSubject() @@ -57,10 +59,17 @@ class CreateScheduleVM { let pointsRelay: BehaviorRelay<[SchedulePointItem]> let dateString: Observable - init() { - pointsRelay = BehaviorRelay<[SchedulePointItem]>(value: [ - SchedulePointItem() - ]) + init(_ model: ScheduleModel?) { + self.scheduModel = model + + // 编辑模式:从 model 回显数据 + if let m = model, !m.points.isEmpty { + pointsRelay = BehaviorRelay<[SchedulePointItem]>(value: m.points.map { $0.toSchedulePointItem() }) + selectedDate.accept(Date(timeIntervalSince1970: TimeInterval(m.timestamp) / 1000)) + selectedGroupKeys = m.groups.map { $0.group_key } + } else { + pointsRelay = BehaviorRelay<[SchedulePointItem]>(value: [SchedulePointItem()]) + } dateString = selectedDate .map { @@ -103,12 +112,4 @@ class CreateScheduleVM { .disposed(by: disposeBag) } - /// 从 LocationPicker 回调更新某个点的位置 - func updatePointLocation(index: Int, name: String, address: String) { - var list = pointsRelay.value - guard index < list.count else { return } - list[index].locationName = name - list[index].address = address - pointsRelay.accept(list) - } } diff --git a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleView.swift b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleView.swift index c3c83b6..e84e875 100644 --- a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleView.swift +++ b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleView.swift @@ -31,14 +31,8 @@ class CreateScheduleView: UIView { AppRouter.shared.popOrDismiss() }).disposed(by: disposeBag) - // scrollView 到达顶部后,由 pan 手势接管 PopView 滑动 - createSchedulePopView.scrollView.rx.contentOffset - .observe(on: MainScheduler.asyncInstance) - .subscribe(onNext: { [weak self] offset in - guard let self = self, self.isSubCanScroll, offset.y <= 0 else { return } - self.createSchedulePopView.scrollView.setContentOffset(.zero, animated: false) - }) - .disposed(by: disposeBag) + // 嵌套滑动协调(参考 GroupView 的 PanScrollView 模式) + createSchedulePopView.scrollView.delegate = self } private func setupUI() { @@ -49,6 +43,7 @@ class CreateScheduleView: UIView { addSubview(navBarView) navBarView.addSubview(backBtn) navBarView.addSubview(navTitleLabel) + navBarView.addSubview(deleteBtn) addSubview(createSchedulePopView) navBgView.layoutChain @@ -68,6 +63,12 @@ class CreateScheduleView: UIView { .top(kStatusBarHeight + 12) .centerX() + deleteBtn.layoutChain + .centerY(navTitleLabel) + .right(15) + .width(16) + .height(16) + #if !targetEnvironment(simulator) mapView.layoutChain .top() @@ -102,14 +103,24 @@ class CreateScheduleView: UIView { case .changed: let newTop = panStartTop + pan.translation(in: self).y + let scrollOffset = createSchedulePopView.scrollView.contentOffset.y if isSubCanScroll { - let scrollOffset = self.createSchedulePopView.scrollView.contentOffset.y - // 内容未到顶部时,不干涉 scrollView 的滑动 + // 内容正在滑动,不移动 PopView if scrollOffset > 0 { return } - // 到顶部后切为 view 拖拽 - isSubCanScroll = false - panStartTop = createSchedulePopView.frame.minY + // 内容滑到顶部,切回 Pan 拖拽(用户下拉时 velocity > 0) + if pan.velocity(in: self).y > 0 || createSchedulePopView.frame.minY > popUpLimit + 1 { + isSubCanScroll = false + panStartTop = createSchedulePopView.frame.minY + } + } else { + // PopView 在顶部且继续上滑 → 激活内容滑动 + if createSchedulePopView.frame.minY <= popUpLimit && newTop <= popUpLimit { + isSubCanScroll = true + panStartTop = createSchedulePopView.frame.minY + topConstraint.constant = popUpLimit + return + } } let clamped = max(popUpLimit, min(popDownLimit, newTop)) @@ -119,23 +130,32 @@ class CreateScheduleView: UIView { let velocity = pan.velocity(in: self) let isNearUp = abs(createSchedulePopView.frame.minY - popUpLimit) < abs(createSchedulePopView.frame.minY - popDownLimit) let target: CGFloat - if abs(velocity.y) > 200 { + if createSchedulePopView.frame.minY <= popUpLimit + 5 { + // 顶部附近:由位置决定去向,速度不触发回收(避免 scrollView 松手误回收) + target = isNearUp ? popUpLimit : popDownLimit + } else if abs(velocity.y) > 200 { target = velocity.y < 0 ? popUpLimit : popDownLimit } else { target = isNearUp ? popUpLimit : popDownLimit } topConstraint.constant = target - // 非最大化时禁用 scollView 滚动 + // 动画前设置 scrollView 状态,避免弹跳 let atTop = target == self.popUpLimit - createSchedulePopView.scrollView.isScrollEnabled = atTop if !atTop { + isSubCanScroll = false + createSchedulePopView.scrollView.isScrollEnabled = false createSchedulePopView.scrollView.setContentOffset(.zero, animated: false) } + isSubCanScroll = atTop - UIView.animate(withDuration: 0.2, delay: 0, - options: [.curveEaseInOut, .allowUserInteraction]) { + UIView.animate(withDuration: 0.35, delay: 0, + usingSpringWithDamping: 0.85, + initialSpringVelocity: abs(velocity.y) / 1000, + options: [.allowUserInteraction]) { self.layoutIfNeeded() + } completion: { _ in + self.createSchedulePopView.scrollView.isScrollEnabled = atTop } default: @@ -181,6 +201,14 @@ class CreateScheduleView: UIView { label.text = "创建行程" return label }() + + lazy var deleteBtn: UIButton = { + let btn = UIButton() + btn.setImage(UIImage(named: "Common/delete"), for: .normal) + btn.extendEdgeInsets = UIEdgeInsets(top: 15, left: 20, bottom: 15, right: 15) + btn.isHidden = true + return btn + }() #if !targetEnvironment(simulator) lazy var mapView: MAMapView! = { @@ -215,7 +243,7 @@ class CreateScheduleView: UIView { } -// MARK: - UIScrollViewDelegate +// MARK: - UIScrollViewDelegate(参考 GroupView 嵌套滑动协调) extension CreateScheduleView: UIScrollViewDelegate { func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { // 内容已有偏移时才允许子滚动 diff --git a/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVipPopView.swift b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVipPopView.swift new file mode 100644 index 0000000..7e3fba5 --- /dev/null +++ b/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVipPopView.swift @@ -0,0 +1,120 @@ +// +// CreateScheduleVipPopView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/25. +// + +import UIKit +import RxSwift +import RxCocoa + +class CreateScheduleVipPopView: UIView { + + private static let shared = CreateScheduleVipPopView(frame: CGRect(origin: .zero, size: kScreenSize)) + + var disposeBag = DisposeBag() + + static func show() { + guard let superView = kKeyWindow else { + return + } + + if CreateScheduleVipPopView.shared.superview != nil { + CreateScheduleVipPopView.shared.removeFromSuperview() + CreateScheduleVipPopView.shared.bgView.frame = .zero + } + CreateScheduleVipPopView.shared.bgView.alpha = 1 + CreateScheduleVipPopView.shared.bgView.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight) + superView.addSubview(CreateScheduleVipPopView.shared) + superView.bringSubviewToFront(CreateScheduleVipPopView.shared) + + UIView.animate(withDuration: 0.25) { + CreateScheduleVipPopView.shared.bgView.alpha = 1 + } + } + + /// 关闭 + static func dismiss() { + guard CreateScheduleVipPopView.shared.superview != nil else { return } + + UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) { + CreateScheduleVipPopView.shared.bgView.alpha = 0 + } completion: { _ in + CreateScheduleVipPopView.shared.removeFromSuperview() + } + } + + private func setupRx() { + upgradedBtn.rx.tap.subscribe(onNext: { _ in + CreateScheduleVipPopView.dismiss() + AppRouter.push(Route.vipRecharge) + }).disposed(by: disposeBag) + + closeBtn.rx.tap.subscribe(onNext: { _ in + CreateScheduleVipPopView.dismiss() + }).disposed(by: disposeBag) + } + + private lazy var bgView: UIView = { + let view = UIView() + view.backgroundColor = .black.withAlphaComponent(0.5) + return view + }() + + lazy var vipImgView: UIImageView = { + let view = UIImageView() + view.image = UIImage(named: "Schedule/vip_pop") + view.contentMode = .scaleAspectFill + return view + }() + + lazy var upgradedBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.backgroundColor = .clear + btn.setBackgroundImage(UIImage(named: "Schedule/upgrade_bg"), for: .normal) + return btn + }() + + lazy var closeBtn: UIButton = { + let btn = UIButton(type: .custom) + btn.backgroundColor = .clear + btn.setBackgroundImage(UIImage(named: "Group/close"), for: .normal) + btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 100, bottom: 100, right: 100) + return btn + }() + + // MARK: - Init + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .clear + addSubview(bgView) + bgView.addSubview(vipImgView) + bgView.addSubview(upgradedBtn) + bgView.addSubview(closeBtn) + + vipImgView.layoutChain + .centerY() + .edgesHorzontal(25) + .heightToWidth(814/648) + + upgradedBtn.layoutChain + .topToBottomOfView(vipImgView, offset: -20) + .centerX() + .width(240) + .height(60) + + closeBtn.layoutChain + .topToBottomOfView(upgradedBtn, offset: 15) + .centerX() + .width(22) + .height(22) + + setupRx() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/QuickLocation/Section/Schedule/LocationPicker/LocationPickerVC.swift b/QuickLocation/Section/Schedule/LocationPicker/LocationPickerVC.swift index 6778415..57b7733 100644 --- a/QuickLocation/Section/Schedule/LocationPicker/LocationPickerVC.swift +++ b/QuickLocation/Section/Schedule/LocationPicker/LocationPickerVC.swift @@ -182,6 +182,7 @@ class LocationPickerVC: BaseViewController { province: poi.province ?? "", city: poi.city ?? "", district: poi.district ?? "", + street: "", formatted_address: poi.address ?? "" ) rootView.resultTableView.isHidden = true @@ -191,6 +192,14 @@ class LocationPickerVC: BaseViewController { rootView.mapView.setCenter(coord, animated: true) rootView.mapView.setZoomLevel(19, animated: true) addLocationAnnotation(coordinate: coord) + // 逆地理编码补充 street + #if !targetEnvironment(simulator) + let regeo = AMapReGeocodeSearchRequest() + regeo.location = AMapGeoPoint.location(withLatitude: CGFloat(coord.latitude), + longitude: CGFloat(coord.longitude)) + regeo.requireExtension = true + searchAPI?.aMapReGoecodeSearch(regeo) + #endif } private func updateTableHeight(itemCount: Int) { @@ -252,11 +261,23 @@ extension LocationPickerVC: AMapSearchDelegate { func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) { guard let regeo = response.regeocode else { return } let address = regeo.formattedAddress ?? "" + // 取第一个 POI 名称作为地点名,没有则取街道/区 + let poiName = regeo.pois?.first?.name + ?? regeo.pois?.first?.address + ?? regeo.pois?.first?.district + ?? "" selectedLocation = PickedLocation( - name: rootView.poiNameLab.text ?? "", - address: address, - coordinate: selectedLocation?.coordinate ?? kCLLocationCoordinate2DInvalid + name: poiName, + address: regeo.pois?.first?.address ?? "", + coordinate: selectedLocation?.coordinate ?? kCLLocationCoordinate2DInvalid, + province: regeo.pois?.first?.province ?? "", + city: regeo.pois?.first?.city ?? "", + district: regeo.pois?.first?.district ?? "", + street: regeo.addressComponent?.streetNumber?.street ?? "", + formatted_address: regeo.pois?.first?.address ?? "" ) + rootView.bottomView.isHidden = false + rootView.poiNameLab.text = poiName rootView.poiAddressLab.text = address } @@ -267,13 +288,25 @@ extension LocationPickerVC: AMapSearchDelegate { // MARK: - MAMapViewDelegate extension LocationPickerVC: MAMapViewDelegate { - func mapView(_ mapView: MAMapView!, didTouchPois pois: [Any]!) { - guard let touchPoi = pois?.first as? MATouchPoi, let uid = touchPoi.uid, !uid.isEmpty else { return } - let request = AMapPOIIDSearchRequest() - request.uid = uid - searchAPI?.aMapPOIIDSearch(request) + func mapView(_ mapView: MAMapView!, didSingleTappedAt coordinate: CLLocationCoordinate2D) { + isShowPoi = true + selectedLocation = PickedLocation( + name: "", + address: "", + coordinate: coordinate + ) + addLocationAnnotation(coordinate: coordinate) + rootView.mapView.setCenter(coordinate, animated: true) + // 逆地理编码获取地址 + #if !targetEnvironment(simulator) + let regeo = AMapReGeocodeSearchRequest() + regeo.location = AMapGeoPoint.location(withLatitude: CGFloat(coordinate.latitude), + longitude: CGFloat(coordinate.longitude)) + regeo.requireExtension = true + searchAPI?.aMapReGoecodeSearch(regeo) + #endif } - + func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! { if annotation is MAUserLocation { return nil } guard annotation is MAPointAnnotation else { return nil } diff --git a/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailVC.swift b/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailVC.swift new file mode 100644 index 0000000..c644123 --- /dev/null +++ b/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailVC.swift @@ -0,0 +1,99 @@ +// +// ScheduleDetailVC.swift +// QuickLocation +// +// Created by 八条 on 2026/6/25. +// + +import UIKit +import RxSwift +import RxCocoa +import RxDataSources +import ObjectMapper + +class ScheduleDetailVC: BaseViewController { + + fileprivate var rootView: ScheduleDetailView! + + override func loadView() { + rootView = ScheduleDetailView(frame: UIScreen.main.bounds) + view = rootView + } + + private var viewModel: ScheduleDetailViewModel + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + setupData() + bindViewModel() + requestFollowList() + + viewModel.loadPointData() + } + + private func bindViewModel() { + viewModel.output.sectionedItems + .bind(to: rootView.collectionView.rx.items(dataSource: dataSource)) + .disposed(by: disposeBag) + + viewModel.output.pointSectionedItems + .bind(to: rootView.tableView.rx.items(dataSource: tableViewDataSource)) + .disposed(by: disposeBag) + } + + private func setupData() { + guard let model = viewModel.scheduModel else { return } + rootView.dateLab.text = rootView.dateLab.getDateInterval2String(date: "\(model.timestamp / 1000)", dateFormat: "yyyy年MM月dd日") + rootView.creatorIcon.image = model.userIcon + } + + // MARK: - API + private func requestFollowList() { + dl.showLoading() + UserService.followList().subscribe(onNext: { response in + self.dl.dismiss() + self.viewModel.loadViewedData(response.list) + self.rootView.noDataLab.isHidden = response.list.count > 0 + }, onError: { _ in }).disposed(by: disposeBag) + } + + // MARK: - dataSource + private lazy var dataSource: RxCollectionViewSectionedReloadDataSource = { + RxCollectionViewSectionedReloadDataSource { datasource, collectionView, indexPath, model in + let cell: ViewedCell = collectionView.dequeueReusableCell(for: indexPath) + cell.configure(model) + return cell + } + }() + + lazy private var tableViewDataSource: RxTableViewSectionedReloadDataSource = { + return RxTableViewSectionedReloadDataSource( + configureCell: { (dataSource, tableView, indexPath, item) in + switch item { + case let .point(model): + let cell: SchedulePointDetailCell = tableView.dequeueReusableCell(for: indexPath) + cell.configure(model: model, index: indexPath.section, total: dataSource.sectionModels.count) + return cell + case let .event(model): + let cell: SchedulePointEventCell = tableView.dequeueReusableCell(for: indexPath) + cell.configure(model: model, index: indexPath.section, total: dataSource.sectionModels.count) + return cell + } + } + ) + }() + + // MARK: - Init + init(routeId: String, scheduleJson: [String: Any]) { + self.viewModel = ScheduleDetailViewModel(routeId: routeId, + model: scheduleJson.isEmpty ? nil : ScheduleModel.init(JSON: scheduleJson)) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailVM.swift b/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailVM.swift new file mode 100644 index 0000000..9718044 --- /dev/null +++ b/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailVM.swift @@ -0,0 +1,65 @@ +// +// ScheduleDetailVM.swift +// QuickLocation +// +// Created by 八条 on 2026/6/25. +// + +import RxSwift +import RxCocoa +import RxDataSources + +enum SchedulePointDetailItem { + case point(model: SchedulePointModel) + case event(model: SchedulePointEventModel) +} + +typealias SchedulePointDetailSectionModel = SectionModel + +struct ScheduleDetailViewModel { + + struct Output { + var sectionedItems: Observable<[ViewedListSectionModel]> + var pointSectionedItems: Observable<[SchedulePointDetailSectionModel]> + } + + let output: Output + + private let sectionedItems = PublishSubject<[ViewedListSectionModel]>() + private let pointSectionedItems = PublishSubject<[SchedulePointDetailSectionModel]>() + + let routeId: String + let scheduModel: ScheduleModel? + + func loadViewedData(_ list: [ViewedModel]) { + sectionedItems.onNext(list.mapSection()) + } + + func loadPointData() { + guard let model = scheduModel else { return } + + var sectionModels: [SchedulePointDetailSectionModel] = [] + var items: [SchedulePointDetailItem] = [] + + for point in model.points { + items.append(.point(model: point)) + for event in point.events { + items.append(.event(model: event)) + } + sectionModels.append(SchedulePointDetailSectionModel(model: point.id, items: items)) + items = [] + } + + pointSectionedItems.onNext(sectionModels) + } + + // MARK: - init + init(routeId: String, model: ScheduleModel?) { + self.routeId = routeId + self.scheduModel = model + output = Output( + sectionedItems: sectionedItems.asObservable(), + pointSectionedItems: pointSectionedItems.asObservable() + ) + } +} diff --git a/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailView.swift b/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailView.swift new file mode 100644 index 0000000..b0555e7 --- /dev/null +++ b/QuickLocation/Section/Schedule/ScheduleDetail/ScheduleDetailView.swift @@ -0,0 +1,534 @@ +// +// ScheduleDetailView.swift +// QuickLocation +// +// Created by 八条 on 2026/6/25. +// + +import UIKit +import RxSwift +import RxCocoa + +class ScheduleDetailView: UIView { + + var disposeBag = DisposeBag() + + private func setupRx() { + backBtn.rx.tap.subscribe(onNext: { _ in + AppRouter.shared.popOrDismiss() + }).disposed(by: disposeBag) + } + + private func setupUI() { + addSubview(navBgView) + addSubview(navBarView) + navBarView.addSubview(navTitleLabel) + navBarView.addSubview(backBtn) + addSubview(headerView) + addSubview(travelRouteView) + addSubview(tableView) + addSubview(bottomView) + + 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) + + headerView.layoutChain + .topToBottomOfView(navBarView) + .edgesHorzontal() + + travelRouteView.layoutChain + .topToBottomOfView(headerView) + .edgesHorzontal() + + bottomView.layoutChain + .edgesHorzontal() + .height(kSafeBottomMargin + 80) + .bottom() + + tableView.layoutChain + .topToBottomOfView(travelRouteView, offset: 15) + .edgesHorzontal() + .bottomToTopOfView(bottomView, offset: 0) + } + + 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 headerView: UIView = { + let view = UIView() + view.backgroundColor = .clear + + let titleLab = UILabel() + titleLab.text = "谁在关注" + titleLab.font = .systemFont(ofSize: 16, weight: .medium) + titleLab.textColor = ThemeManager.shared.color.titleAuxColor + view.addSubview(titleLab) + titleLab.layoutChain + .top(15) + + let dotView = UIView() + dotView.backgroundColor = UIColor(hexStr: "#16B3FF") + dotView.cornerRadius = 2 + view.addSubview(dotView) + dotView.layoutChain + .left(15) + .centerY(titleLab) + .width(4) + .height(11) + + titleLab.layoutChain.leftToRightOfView(dotView, offset: 5) + + view.addSubview(collectionView) + collectionView.layoutChain + .topToBottomOfView(titleLab, offset: 15) + .edgesHorzontal() + .height(80) + .bottom(15) + + view.addSubview(noDataLab) + noDataLab.layoutChain + .centerX().centerY() + + return view + }() + + lazy var collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.itemSize = CGSize(width: 80, height: 80) + layout.minimumLineSpacing = 15 + layout.sectionInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15) + let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) + cv.backgroundColor = .clear + cv.showsHorizontalScrollIndicator = false + cv.register(ViewedCell.self) + return cv + }() + + lazy var noDataLab: UILabel = { + let label = UILabel() + label.text = " 暂无关注" + label.textColor = UIColor(hexStr: "#999999") + label.font = .systemFont(ofSize: 14, weight: .regular) + label.isHidden = true + return label + }() + + /// 行程日期 + lazy var travelRouteView: UIView = { + let view = UIView() + view.backgroundColor = .clear + + let titleLab = UILabel() + titleLab.text = "行程日期" + titleLab.font = .systemFont(ofSize: 16, weight: .medium) + titleLab.textColor = ThemeManager.shared.color.titleAuxColor + view.addSubview(titleLab) + titleLab.layoutChain + .top(5) + + let dotView = UIView() + dotView.backgroundColor = UIColor(hexStr: "#16B3FF") + dotView.cornerRadius = 2 + view.addSubview(dotView) + dotView.layoutChain + .left(15) + .centerY(titleLab) + .width(4) + .height(11) + + titleLab.layoutChain.leftToRightOfView(dotView, offset: 5) + + view.addSubview(dateLab) + dateLab.layoutChain + .centerY(titleLab) + .leftToRightOfView(titleLab, offset: 10) + + view.addSubview(creatorIcon) + creatorIcon.layoutChain + .right(15) + .width(30) + .height(30) + .centerY(titleLab) + + let creatorTitleLab = UILabel() + creatorTitleLab.text = "创建人" + creatorTitleLab.font = .systemFont(ofSize: 12, weight: .medium) + creatorTitleLab.textColor = ThemeManager.shared.color.titleAuxColor + view.addSubview(creatorTitleLab) + creatorTitleLab.layoutChain + .rightToLeftOfView(creatorIcon, offset: -5) + .centerY(titleLab) + + view.addSubview(vipTipsLab) + vipTipsLab.layoutChain + .topToBottomOfView(titleLab, offset: 5) + .leftToView(titleLab) + .bottom() + + return view + }() + + lazy var dateLab: UILabel = { + let label = UILabel() + label.textColor = UIColor(hexStr: "#16B3FF") + label.font = .systemFont(ofSize: 14, weight: .medium) + return label + }() + + lazy var creatorIcon: UIImageView = { + let view = UIImageView() + view.cornerRadius = 15 + return view + }() + + lazy var vipTipsLab: UILabel = { + let label = UILabel() + label.textColor = UIColor(hexStr: "#FF7D52") + label.font = .systemFont(ofSize: 10, weight: .regular) + return label + }() + + lazy var tableView: UITableView = { + let tv = UITableView(frame: .zero, style: .plain) + tv.backgroundColor = .clear + tv.separatorStyle = .none + tv.estimatedRowHeight = 77 + tv.showsVerticalScrollIndicator = false + tv.bounces = false + tv.register(SchedulePointDetailCell.self) + tv.register(SchedulePointEventCell.self) + tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0) + return tv + }() + + lazy var bottomView: UIView = { + let v = UIView() + v.backgroundColor = .white + v.layer.cornerRadius = 16 + v.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + v.layer.shadowColor = UIColor.black.withAlphaComponent(0.1).cgColor + v.layer.shadowOffset = CGSize(width: 0, height: -2) + v.layer.shadowRadius = 10 + v.layer.shadowOpacity = 1 + return v + }() + + lazy var operateBtn: UIButton = { + let btn = UIButton() + 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 = 25 + return btn + }() + + override init(frame: CGRect) { + super.init(frame: .zero) + backgroundColor = .white + setupUI() + setupRx() + + vipTipsLab.text = AppContextManager.shared.vip > 1 ? "" : "升级 VIP,查看具体事件" + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - SchedulePointDetailCell +class SchedulePointDetailCell: UITableViewCell { + + var disposeBag = DisposeBag() + + func configure(model: SchedulePointModel, index: Int, total: Int) { + indexLabel.text = "\(index + 1)" + locationLabel.text = model.street + remarkLab.text = model.remark.isEmpty ? "备注:无备注" : model.remark + timeLabel.text = getDateInterval2String(date: "\(model.expected_timestamp / 1000)", dateFormat: "HH:mm") + + var indexName = "" + if index == 0 { + indexName = "起点:" + } + else if index == total - 1 { + indexName = "终点:" + } + else { + indexName = "途经点:" + } + indexNameLab.text = indexName + bottomDashView.isHidden = index == total - 1 + } + + // MARK: - Init + override init(style: CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + backgroundColor = .clear + setupViews() + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func prepareForReuse() { + super.prepareForReuse() + disposeBag = DisposeBag() + } + + private func setupViews() { + contentView.addSubview(pointIcon) + contentView.addSubview(bottomDashView) + contentView.addSubview(indexLabel) + contentView.addSubview(indexNameLab) + contentView.addSubview(locationLabel) + contentView.addSubview(timeLabel) + contentView.addSubview(remarkLab) + + pointIcon.layoutChain + .top() + .left(15) + .width(18).height(18) + + indexLabel.layoutChain + .top().leftToRightOfView(pointIcon, offset: 10) + .width(22).height(20) + + indexNameLab.layoutChain + .leftToRightOfView(indexLabel, offset: 5) + .centerY(indexLabel) + + locationLabel.layoutChain + .centerY(indexLabel) + .leftToRightOfView(indexNameLab, offset: 2) + .compressionHorizontal(.defaultLow) + + timeLabel.layoutChain + .centerY(indexLabel) + .leftToRightOfView(locationLabel, offset: 20) + .right(10, relation: .greaterThanOrEqual) + .compressionHorizontal(.required) + + remarkLab.layoutChain + .topToBottomOfView(indexLabel, offset: 5) + .leftToView(indexLabel) + .right(15) + .bottom(10) + .compressionVertical(.required) + + bottomDashView.layoutChain + .topToBottomOfView(pointIcon, offset: 5) + .centerX(pointIcon) + .width(0.5) + .bottom(5) + } + + // MARK: - Views + private let pointIcon: UIImageView = { + let iv = UIImageView(image: UIImage(named: "Schedule/point")) + iv.contentMode = .scaleAspectFit + return iv + }() + + private let bottomDashView: DashLineView = { + let v = DashLineView() + v.backgroundColor = .clear + return v + }() + + private let indexLabel: UILabel = { + let l = UILabel() + l.backgroundColor = UIColor(hexStr: "#DCF4FF") + l.textColor = UIColor(hexStr: "#176F9B") + l.font = .systemFont(ofSize: 12, weight: .medium) + l.textAlignment = .center + return l + }() + + lazy var indexNameLab: UILabel = { + let l = UILabel() + l.font = .systemFont(ofSize: 14, weight: .medium) + l.textColor = UIColor(hexStr: "#16B3FF") + return l + }() + + private let locationLabel: UILabel = { + let l = UILabel() + l.font = .systemFont(ofSize: 14, weight: .medium) + l.textColor = UIColor(hexStr: "#333333") +// l.numberOfLines = 0 + return l + }() + + private let timeLabel: UILabel = { + let l = UILabel() + l.font = .systemFont(ofSize: 12, weight: .medium) + l.textColor = UIColor(hexStr: "#333333") + l.isUserInteractionEnabled = true + l.textAlignment = .right + return l + }() + + lazy var remarkLab: UILabel = { + let l = UILabel() + l.font = .systemFont(ofSize: 12, weight: .regular) + l.textColor = UIColor(hexStr: "#999999") + l.numberOfLines = 0 + return l + }() + + override func layoutSubviews() { + super.layoutSubviews() + contentView.layoutIfNeeded() + indexLabel.setCornerRadius(corners: [.bottomRight, .topLeft], withCornerRadii: CGSize(width: 10, height: 10)) + } +} + +// MARK: - SchedulePointEventCell +class SchedulePointEventCell: UITableViewCell { + + var disposeBag = DisposeBag() + + func configure(model: SchedulePointEventModel, index: Int, total: Int) { + var suffix = "" + if index == 0 { + suffix = " 起点" + } else if index == total - 1 { + suffix = " 终点" + } else { + suffix = " 途经点" + } + infoLab.text = model.text + "到达" + suffix + } + + // MARK: - Init + override init(style: CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + backgroundColor = .clear + setupViews() + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func prepareForReuse() { + super.prepareForReuse() + disposeBag = DisposeBag() + } + + private func setupViews() { + contentView.addSubview(verticalDashLineView) + contentView.addSubview(horizontalDashLineView) + contentView.addSubview(dotView) + contentView.addSubview(infoView) + infoView.addSubview(infoLab) + + verticalDashLineView.layoutChain + .left(24) + .edgesVertical() + .width(1) + + dotView.layoutChain + .centerX(verticalDashLineView) + .centerY() + .width(8) + .height(8) + + horizontalDashLineView.layoutChain + .leftToRightOfView(dotView) + .centerY(dotView) + .height(1) + .width(30) + + infoView.layoutChain + .leftToRightOfView(horizontalDashLineView, offset: 5) + .edgesVertical(10) + .right(15) + + infoLab.layoutChain + .edgesHorzontal(15) + .edgesVertical(15) + } + + // MARK: - Views + lazy var verticalDashLineView: DashLineView = { + let v = DashLineView() + v.backgroundColor = .clear + return v + }() + + lazy var horizontalDashLineView: HorizontalDashLineView = { + let v = HorizontalDashLineView() + v.backgroundColor = .clear + return v + }() + + lazy var dotView: UIView = { + let view = UIView() + view.backgroundColor = UIColor(hexStr: "#16B3FF") + view.cornerRadius = 4 + return view + }() + + lazy var infoView: UIView = { + let v = UIView() + v.backgroundColor = .white + v.layer.cornerRadius = 10 + v.layer.shadowColor = UIColor.black.withAlphaComponent(0.1).cgColor + v.layer.shadowOffset = CGSize(width: 0, height: -2) + v.layer.shadowRadius = 10 + v.layer.shadowOpacity = 1 + return v + }() + + lazy var infoLab: UILabel = { + let l = UILabel() + l.font = .systemFont(ofSize: 12, weight: .medium) + l.textColor = UIColor(hexStr: "#333333") + return l + }() +} diff --git a/QuickLocation/Section/Schedule/ScheduleModel.swift b/QuickLocation/Section/Schedule/ScheduleModel.swift index 5b4cc59..b2a1c3b 100644 --- a/QuickLocation/Section/Schedule/ScheduleModel.swift +++ b/QuickLocation/Section/Schedule/ScheduleModel.swift @@ -64,10 +64,8 @@ struct ScheduleModel: Mappable, Equatable { var is_own: Bool = false /// 行程点 var points: [SchedulePointModel] = [] - /// 圈子名 -// var group_name: String = "" - /// 圈子key -// var group_key: String = "" + /// 圈子 + var groups: [ScheduleGroupModel] = [] init?(map: Map) { @@ -82,8 +80,7 @@ struct ScheduleModel: Mappable, Equatable { is_follow <- map["is_follow"] is_own <- map["is_own"] points <- map["points"] -// group_name <- map["groups.group_name"] -// group_key <- map["groups.group_key"] + groups <- map["groups"] } } @@ -118,7 +115,7 @@ struct SchedulePointModel: Mappable, Equatable { /// 备注 var remark: String = "" /// 事件 - var eventText: String = "" + var events: [SchedulePointEventModel] = [] /// 节点顺序 var sequence: Int = 0 @@ -133,7 +130,7 @@ struct SchedulePointModel: Mappable, Equatable { longitude <- map["location.longitude"] latitude <- map["location.latitude"] remark <- map["remark"] - eventText <- map["events.text"] + events <- map["events"] province <- map["address.province"] district <- map["address.district"] country <- map["address.country"] @@ -146,8 +143,89 @@ struct SchedulePointModel: Mappable, Equatable { extension SchedulePointModel: IdentifiableType { public typealias Identity = String - + public var identity: String { return id } } + +extension SchedulePointModel { + /// 转为创建行程用的 SchedulePointItem + func toSchedulePointItem() -> SchedulePointItem { + var item = SchedulePointItem() + item.locationName = formatted_address + item.address = formatted_address + item.latitude = latitude ?? 0 + item.longitude = longitude ?? 0 + item.province = province + item.city = city + item.district = district + item.street = street + item.country = country + item.formatted_address = formatted_address + if expected_timestamp > 0 { + item.expectedTime = Date(timeIntervalSince1970: TimeInterval(expected_timestamp) / 1000) + } + item.remark = remark + return item + } +} + +/// 行程点事件 +struct SchedulePointEventModel: Mappable, Equatable { + var uuid: String = UUID().uuidString + /// 事件内容 + var text: String = "" + /// 到达时间 + var arrival_timestamp: Int64 = 0 + /// + var user_id: String = "" + var nick_name: String = "" + var point_id: String = "" + + init?(map: Map) { + + } + + mutating func mapping(map: Map) { + text <- map["text"] + arrival_timestamp <- map["arrival_timestamp"] + user_id <- map[user_id] + nick_name <- map["nick_name"] + point_id <- map["point_id"] + } +} + +extension SchedulePointEventModel: IdentifiableType { + public typealias Identity = String + + public var identity: String { + return uuid + } +} + +/// 行程分享的圈子 +struct ScheduleGroupModel: Mappable, Equatable { + var uuid: String = UUID().uuidString + /// id + var group_key: String = "" + /// 圈子名 + var group_name: String = "" + + init?(map: Map) { + + } + + mutating func mapping(map: Map) { + group_key <- map["group_key"] + group_name <- map["group_name"] + } +} + +extension ScheduleGroupModel: IdentifiableType { + public typealias Identity = String + + public var identity: String { + return uuid + } +} diff --git a/QuickLocation/Section/Schedule/ScheduleVC.swift b/QuickLocation/Section/Schedule/ScheduleVC.swift index 9aeb46f..623813e 100644 --- a/QuickLocation/Section/Schedule/ScheduleVC.swift +++ b/QuickLocation/Section/Schedule/ScheduleVC.swift @@ -10,6 +10,7 @@ import RxSwift import RxCocoa import RxDataSources import MJRefresh +import ObjectMapper class ScheduleVC: BaseViewController { @@ -30,10 +31,14 @@ class ScheduleVC: BaseViewController { bindViewModel() reactiveAction() - requestData() requestFollowList() } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + requestData() + } + private func bindViewModel() { viewModel.output.sectionedItems .bind(to: rootView.collectionView.rx.items(dataSource: dataSource)) @@ -48,6 +53,10 @@ class ScheduleVC: BaseViewController { self.dl.dismiss() self.rootView.tableView.refresh(status: status, isEmpty: isEmpty) }).disposed(by: disposeBag) + + rootView.tableView.rx.modelSelected(ScheduleModel.self) + .subscribe(viewModel.cellAction.inputs) + .disposed(by: disposeBag) } private func reactiveAction() { @@ -98,6 +107,17 @@ class ScheduleVC: BaseViewController { configureCell: { (_, tableView, indexPath, model) in let cell: ScheduleListPopCell = tableView.dequeueReusableCell(for: indexPath) cell.configure(model) + + // 编辑 + cell.editBtn.rx.tap.subscribe(onNext: { _ in + AppRouter.push(Route.createSchedule, userInfo: ["scheduleJson": model.toJSON()]) + }).disposed(by: cell.disposeBag) + + // 关注 + cell.followBtn.rx.tap.subscribe(onNext: { _ in + + }).disposed(by: cell.disposeBag) + return cell } ) diff --git a/QuickLocation/Section/Schedule/ScheduleView.swift b/QuickLocation/Section/Schedule/ScheduleView.swift index b75eac0..f616127 100644 --- a/QuickLocation/Section/Schedule/ScheduleView.swift +++ b/QuickLocation/Section/Schedule/ScheduleView.swift @@ -175,7 +175,9 @@ class ScheduleView: UIView { tableView.backgroundColor = .clear tableView.separatorStyle = .none tableView.estimatedRowHeight = 80 + tableView.showsVerticalScrollIndicator = false tableView.register(ScheduleListPopCell.self) + tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0) return tableView }() @@ -222,7 +224,7 @@ final class ViewedCell: UICollectionViewCell { }() lazy var blurView: UIVisualEffectView = { - let blurEffect = UIBlurEffect(style: .systemUltraThinMaterialLight) + let blurEffect = UIBlurEffect(style: .systemUltraThinMaterial) let view = UIVisualEffectView(effect: blurEffect) return view }() @@ -270,11 +272,14 @@ final class ViewedCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } + } // MARK: - ScheduleListPopCell class ScheduleListPopCell: UITableViewCell { + var disposeBag = DisposeBag() + func configure(_ model: ScheduleModel) { nameLab.text = model.nick_name + " 的行程路线" monthLab.text = getDateInterval2String(date: "\(model.timestamp/1000)", dateFormat: "MM月") @@ -355,6 +360,11 @@ class ScheduleListPopCell: UITableViewCell { // Configure the view for the selected state } + override func prepareForReuse() { + super.prepareForReuse() + disposeBag = DisposeBag() + } + lazy var bgView: UIView = { let view = UIView() view.backgroundColor = .clear diff --git a/QuickLocation/Section/Schedule/ScheduleViewModel.swift b/QuickLocation/Section/Schedule/ScheduleViewModel.swift index 35c1944..bd66e43 100644 --- a/QuickLocation/Section/Schedule/ScheduleViewModel.swift +++ b/QuickLocation/Section/Schedule/ScheduleViewModel.swift @@ -9,6 +9,7 @@ import RxSwift import RxCocoa import RxDataSources import MJRefresh +import ObjectMapper typealias ViewedListSectionModel = SectionModel typealias ScheduleListSectionModel = AnimatableSectionModel @@ -47,6 +48,13 @@ class ScheduleViewModel: ViewModelType { sectionedItems.onNext(list.mapSection()) } + lazy var cellAction: Action = { this in + return Action { model in + AppRouter.push(Route.scheduleDetail, userInfo: ["scheduleJson": model.toJSON()]) + return .empty() + } + }(self) + // MARK: - init init() { listService = ItineraryService.query() diff --git a/QuickLocation/Service/ItineraryService.swift b/QuickLocation/Service/ItineraryService.swift index b4c7375..4595eef 100644 --- a/QuickLocation/Service/ItineraryService.swift +++ b/QuickLocation/Service/ItineraryService.swift @@ -11,7 +11,7 @@ import Moya struct ItineraryService { static let disposeBag = DisposeBag() - /// 查询行程 + /// 查询行程列表 /// - Parameters: /// - follow: 只查看关注的行程 /// - own: 只查看自己创建的行程 @@ -30,4 +30,36 @@ struct ItineraryService { } } + /// 查询行程关注人 + /// - Parameters: + /// - id: 行程ID + static func queryFollowList(id: String) -> Observable { + let api = ItineraryAPI.queryFollowList(id: id).multiTarget + return APIProvider.request(token: api) + .map(ResponseModel.self) + .asObservable() + } + + /// 设置行程 + /// - Parameters: + /// - id: 更新时传入 + /// - group_keys: 只查看关注的行程 + /// - timestamp: 行程日期 + /// - points: 行程点 + static func set(id: String="", group_keys: [String], timestamp: Int64, points: [[String: Any]]) -> Observable { + let api = ItineraryAPI.set(id: id, group_keys: group_keys, timestamp: timestamp, points: points).multiTarget + return APIProvider.request(token: api) + .map(ResponseModel.self) + .asObservable() + } + + /// 删除 + /// - Parameters: + /// - id: 行程ID + static func delete(id: String) -> Observable { + let api = ItineraryAPI.delete(id: id).multiTarget + return APIProvider.request(token: api) + .map(ResponseModel.self) + .asObservable() + } }