- 创建行程接口调用

- 编辑行程接口调用
- 行程详情UI
This commit is contained in:
linshujie 2026-06-25 18:36:48 +08:00
parent 47a02a9e07
commit e438736fb4
33 changed files with 1469 additions and 108 deletions

View File

@ -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 = "<group>"; };
30BAB8622FCD716C00C33B5C /* JoinGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinGroupVC.swift; sourceTree = "<group>"; };
30BAB8642FCD718A00C33B5C /* JoinGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinGroupView.swift; sourceTree = "<group>"; };
30BF300B2FED09BA00D9CB52 /* ScheduleDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleDetailView.swift; sourceTree = "<group>"; };
30BF300D2FED09CC00D9CB52 /* ScheduleDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleDetailVC.swift; sourceTree = "<group>"; };
30BF300F2FED0C8E00D9CB52 /* ScheduleDetailVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScheduleDetailVM.swift; sourceTree = "<group>"; };
30C4C0112FDABC8C009215C1 /* QuickLocation.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QuickLocation.entitlements; sourceTree = "<group>"; };
30C4C0152FDB91B8009215C1 /* CheckPermissionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckPermissionVC.swift; sourceTree = "<group>"; };
30C4C0182FDBF094009215C1 /* RemoveMemberVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveMemberVC.swift; sourceTree = "<group>"; };
@ -476,6 +483,7 @@
30D87D022FE1336300E958FD /* NavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationView.swift; sourceTree = "<group>"; };
30D891F42FE22E0600E958FD /* OrderAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderAPI.swift; sourceTree = "<group>"; };
30D891F62FE22E6E00E958FD /* OrderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderService.swift; sourceTree = "<group>"; };
30DA36BC2FECC5AB008D5A2C /* CreateScheduleVipPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleVipPopView.swift; sourceTree = "<group>"; };
30DC18512FD009CD0041DCD1 /* VipExpenseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipExpenseModel.swift; sourceTree = "<group>"; };
30DC18532FD00C4A0041DCD1 /* VipRechargeVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipRechargeVM.swift; sourceTree = "<group>"; };
30DC18552FD11E7A0041DCD1 /* NavigationTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTitleView.swift; sourceTree = "<group>"; };
@ -1226,6 +1234,16 @@
path = Join;
sourceTree = "<group>";
};
30BF300A2FED09A300D9CB52 /* ScheduleDetail */ = {
isa = PBXGroup;
children = (
30BF300D2FED09CC00D9CB52 /* ScheduleDetailVC.swift */,
30BF300F2FED0C8E00D9CB52 /* ScheduleDetailVM.swift */,
30BF300B2FED09BA00D9CB52 /* ScheduleDetailView.swift */,
);
path = ScheduleDetail;
sourceTree = "<group>";
};
30C4C0122FDB9178009215C1 /* CheckPermission */ = {
isa = PBXGroup;
children = (
@ -1283,6 +1301,7 @@
30D74AB92FEA37AD0050EB2C /* ScheduleModel.swift */,
30D74ABB2FEA67CE0050EB2C /* CreateSchedule */,
30D74BF22FEB6F5B0050EB2C /* LocationPicker */,
30BF300A2FED09A300D9CB52 /* ScheduleDetail */,
);
path = Schedule;
sourceTree = "<group>";
@ -1294,6 +1313,7 @@
30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */,
30D74ABE2FEA67F30050EB2C /* CreateScheduleView.swift */,
30D74AC02FEA6EEF0050EB2C /* CreateSchedulePopView.swift */,
30DA36BC2FECC5AB008D5A2C /* CreateScheduleVipPopView.swift */,
);
path = CreateSchedule;
sourceTree = "<group>";
@ -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 */,

View File

@ -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())
}
}
}

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 KiB

View File

@ -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])
}
}
}

View File

@ -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)
}
}

View File

@ -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<SchedulePointSection>(
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

View File

@ -32,6 +32,8 @@ class CreateScheduleVM {
let disposeBag = DisposeBag()
let scheduModel: ScheduleModel?
// MARK: - Input
let addPointTapped = PublishSubject<Void>()
let deletePointAt = PublishSubject<Int>()
@ -57,10 +59,17 @@ class CreateScheduleVM {
let pointsRelay: BehaviorRelay<[SchedulePointItem]>
let dateString: Observable<String>
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)
}
}

View File

@ -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) {
//

View File

@ -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")
}
}

View File

@ -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 }

View File

@ -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<ViewedListSectionModel> = {
RxCollectionViewSectionedReloadDataSource<ViewedListSectionModel> { datasource, collectionView, indexPath, model in
let cell: ViewedCell = collectionView.dequeueReusableCell(for: indexPath)
cell.configure(model)
return cell
}
}()
lazy private var tableViewDataSource: RxTableViewSectionedReloadDataSource<SchedulePointDetailSectionModel> = {
return RxTableViewSectionedReloadDataSource<SchedulePointDetailSectionModel>(
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")
}
}

View File

@ -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<String, SchedulePointDetailItem>
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()
)
}
}

View File

@ -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
}()
}

View File

@ -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
}
}

View File

@ -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
}
)

View File

@ -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

View File

@ -9,6 +9,7 @@ import RxSwift
import RxCocoa
import RxDataSources
import MJRefresh
import ObjectMapper
typealias ViewedListSectionModel = SectionModel<String, ViewedModel>
typealias ScheduleListSectionModel = AnimatableSectionModel<String, ScheduleModel>
@ -47,6 +48,13 @@ class ScheduleViewModel: ViewModelType {
sectionedItems.onNext(list.mapSection())
}
lazy var cellAction: Action<ScheduleModel, Void> = { this in
return Action { model in
AppRouter.push(Route.scheduleDetail, userInfo: ["scheduleJson": model.toJSON()])
return .empty()
}
}(self)
// MARK: - init
init() {
listService = ItineraryService.query()

View File

@ -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<ResponseModel> {
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<ResponseModel> {
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<ResponseModel> {
let api = ItineraryAPI.delete(id: id).multiTarget
return APIProvider.request(token: api)
.map(ResponseModel.self)
.asObservable()
}
}