// // CreateSchedulePopView.swift // QuickLocation // // Created by 八条 on 2026/6/23. // import UIKit import RxSwift import RxCocoa import RxDataSources import BRPickerView import RxGesture import CoreLocation import TagListView import SwiftDate // MARK: - CreateSchedulePopView class CreateSchedulePopView: UIView { var disposeBag = DisposeBag() /// 途经点拖拽排序中,外层 Pop pan 应让路 var isReorderingPoints = false func setupTagData(_ list: [GroupInfoModel]) { let nameArr = list.map { $0.name } tagListView.removeAllTags() tagListView.addTags(nameArr) tagListView.tagViews.forEach { $0.layer.cornerRadius = 4 $0.layer.borderWidth = 1 $0.layer.borderColor = UIColor.clear.cgColor } tagListView.setNeedsLayout() tagListView.layoutIfNeeded() } // MARK: - UI private func setupUI() { addSubview(lineView) addSubview(headerView) addSubview(scrollView) lineView.layoutChain .top(15) .centerX() .width(36) .height(4) headerView.layoutChain .topToBottomOfView(lineView, offset: 16) .edgesHorzontal() .height(24) scrollView.layoutChain .topToBottomOfView(headerView, offset: 16) .edges(excludingEdge: .top) } // MARK: - Views lazy var lineView: UIView = { let v = UIView() v.backgroundColor = UIColor(hexStr: "#EBEBEB") v.cornerRadius = 2 return v }() lazy var headerView: UIView = { let view = UIView() view.backgroundColor = .clear let pin = UIImageView(image: UIImage(named: "Schedule/title_location")) view.addSubview(pin) pin.layoutChain.left(16).centerY().width(18).height(18) let titleLab = UILabel() titleLab.text = "行程详情" titleLab.font = .systemFont(ofSize: 16, weight: .bold) titleLab.textColor = UIColor(hexStr: "#293445") view.addSubview(titleLab) titleLab.layoutChain.leftToRightOfView(pin, offset: 6).centerY() let icon = UIImageView(image: UIImage(named: "Common/arrow_right")) view.addSubview(icon) icon.layoutChain .right(15) .centerY() .width(10) .height(10) view.addSubview(dateLab) dateLab.layoutChain.rightToLeftOfView(icon, offset: -6).centerY() return view }() lazy var dateLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 13, weight: .medium) label.textColor = UIColor(hexStr: "#293445") label.isUserInteractionEnabled = true label.text = Date().toFormat("yyyy年MM月dd日") return label }() /// 兼容旧绑定:标题旁 + 已移除,保留空按钮避免崩溃 lazy var addBtn: UIButton = { let btn = UIButton(type: .custom) btn.isHidden = true return btn }() lazy var scrollView: UIScrollView = { let view = UIScrollView() view.backgroundColor = .clear view.showsHorizontalScrollIndicator = false view.bounces = false view.delaysContentTouches = false let contentView = UIView() contentView.backgroundColor = .clear view.addSubview(contentView) contentView.layoutChain .edges().widthToView(view) contentView.addSubview(tableView) tableView.layoutChain .top() .edgesHorzontal() contentView.addSubview(shareGroupView) shareGroupView.layoutChain .topToBottomOfView(tableView, offset: 8) .edgesHorzontal() contentView.addSubview(createBtn) createBtn.layoutChain .topToBottomOfView(shareGroupView, offset: 24) .edgesHorzontal(16) .height(50) .bottom(kSafeBottomMargin) return view }() lazy var tableView: UITableView = { let tv = UITableView(frame: .zero, style: .plain) tv.backgroundColor = .clear tv.separatorStyle = .none tv.estimatedRowHeight = ScheduleTimelineMetric.pointRowHeight tv.rowHeight = UITableView.automaticDimension tv.isScrollEnabled = false tv.showsVerticalScrollIndicator = false tv.bounces = false tv.clipsToBounds = false tv.register(SchedulePointCell.self) tv.register(ScheduleAddPointCell.self) return tv }() lazy var shareGroupView: UIView = { let view = UIView() view.backgroundColor = .clear let pin = UIImageView(image: UIImage(named: "Schedule/location")?.withRenderingMode(.alwaysTemplate)) pin.tintColor = UIColor(hexStr: "#16B3FF") pin.contentMode = .scaleAspectFit view.addSubview(pin) pin.layoutChain.left(16).top().width(14).height(14) let sectionLab = UILabel() sectionLab.text = "其他" sectionLab.font = .systemFont(ofSize: 16, weight: .bold) sectionLab.textColor = UIColor(hexStr: "#293445") view.addSubview(sectionLab) sectionLab.layoutChain.leftToRightOfView(pin, offset: 6).centerY(pin) let card = UIView() card.backgroundColor = .white card.layer.cornerRadius = 12 view.addSubview(card) card.layoutChain .topToBottomOfView(sectionLab, offset: 12) .edgesHorzontal(16) .bottom() let titleLab = UILabel() titleLab.text = "分享的圈子" titleLab.font = .systemFont(ofSize: 14, weight: .medium) titleLab.textColor = UIColor(hexStr: "#1A1A1A") card.addSubview(titleLab) titleLab.layoutChain.left(16).top(19) card.addSubview(tagListView) tagListView.layoutChain .topToBottomOfView(titleLab, offset: 12) .edgesHorzontal(16) .bottom(12) return view }() lazy var tagListView: TagListView = { let view = TagListView() view.textFont = UIFont.systemFont(ofSize: 12, weight: .medium) view.textColor = UIColor(hexStr: "#AAAAAA") view.tagBackgroundColor = UIColor(hexStr: "#F0F0F0") view.tagBorderColor = UIColor.clear view.selectedTextColor = UIColor(hexStr: "#00ADFE") view.tagSelectedBackgroundColor = UIColor(hexStr: "#E3F6FF") view.selectedBorderColor = UIColor(hexStr: "#16B3FF") view.paddingX = 15 view.paddingY = 10 view.alignment = .left view.translatesAutoresizingMaskIntoConstraints = false return view }() lazy var createBtn: UIButton = { let btn = UIButton(type: .custom) btn.setTitle("发布行程", for: .normal) btn.setTitleColor(.white, for: .normal) btn.titleLabel?.font = FontManager.boboBold(18) btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal) btn.cornerRadius = 16 return btn }() override init(frame: CGRect) { super.init(frame: frame) backgroundColor = UIColor(hexStr: "#FAFAFA") layer.cornerRadius = 30 layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] layer.masksToBounds = true setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: - Timeline metrics enum ScheduleTimelineMetric { /// marker left 16 + width 22 / 2 static let centerX: CGFloat = 27 static let markerSize: CGFloat = 18 /// 8 + 10+36+8+36+8+36+10 + 8,需与卡片内部约束严格一致,否则会冲突 static let pointRowHeight: CGFloat = 160 static let addRowHeight: CGFloat = 60 } // MARK: - SchedulePointCell final class SchedulePointCell: UITableViewCell { static let reuseId = "SchedulePointCell" var disposeBag = DisposeBag() var onLocationTap: (() -> Void)? var onTimeTap: (() -> Void)? var onDelete: (() -> Void)? /// 拖拽手柄长按手势(began/changed/ended),由 VC 处理跟手排序 var onDragGesture: ((UILongPressGestureRecognizer) -> Void)? private let dashLineView = ScheduleDashLineView() private let markerView = UIView() private let markerLab = UILabel() private let middleDot = UIView() private let cardView = UIView() private let locationRow = ScheduleFieldRowView( icon: UIImage(named: "Schedule/location"), placeholder: "请选择" ) private let timeRow = ScheduleFieldRowView( icon: UIImage(named: "Schedule/clock"), placeholder: "请选择" ) private let remarkRow = ScheduleFieldRowView( icon: UIImage(named: "Schedule/edit"), placeholder: "在此添加备注", editable: true ) private let dragBtn = UIButton(type: .custom) private let deleteBtn = UIButton(type: .custom) private var locationRowRightConstraint: NSLayoutConstraint? private var remarkRowRightConstraint: NSLayoutConstraint? private var dragPress: UILongPressGestureRecognizer? private var dashTopToContent: NSLayoutConstraint? private var dashTopToMarkerCenter: NSLayoutConstraint? private var dashBottomToContent: NSLayoutConstraint? private var dashBottomToMarkerCenter: NSLayoutConstraint? func configure(item: SchedulePointItem, role: SchedulePointRole) { switch role { case .start: markerView.isHidden = false middleDot.isHidden = true markerView.backgroundColor = UIColor(hexStr: "#16B3FF") markerLab.text = "始" markerLab.isHidden = false dragBtn.isHidden = true deleteBtn.isHidden = true locationRowRightConstraint?.constant = -10 remarkRowRightConstraint?.constant = -10 // 虚线从始圆点开始往下 dashTopToContent?.isActive = false dashTopToMarkerCenter?.isActive = true dashBottomToMarkerCenter?.isActive = false dashBottomToContent?.isActive = true case .middle: markerView.isHidden = true middleDot.isHidden = false markerLab.isHidden = true dragBtn.isHidden = false deleteBtn.isHidden = false locationRowRightConstraint?.constant = -40 remarkRowRightConstraint?.constant = -40 // 虚线贯穿整行 dashTopToMarkerCenter?.isActive = false dashTopToContent?.isActive = true dashBottomToMarkerCenter?.isActive = false dashBottomToContent?.isActive = true case .end: markerView.isHidden = false middleDot.isHidden = true markerView.backgroundColor = UIColor(hexStr: "#FF5A5A") markerLab.text = "终" markerLab.isHidden = false dragBtn.isHidden = true deleteBtn.isHidden = true locationRowRightConstraint?.constant = -10 remarkRowRightConstraint?.constant = -10 // 虚线到终圆点结束 dashTopToMarkerCenter?.isActive = false dashTopToContent?.isActive = true dashBottomToContent?.isActive = false dashBottomToMarkerCenter?.isActive = true } let locationText = item.street.isEmpty ? (item.locationName.isEmpty ? nil : item.locationName) : item.street locationRow.setText(locationText) if let expectedTime = item.expectedTime { timeRow.setText(expectedTime.toFormat("HH:mm")) } else { timeRow.setText(nil) } remarkRow.setText(item.remark.isEmpty ? nil : item.remark) remarkRow.textField?.text = item.remark locationRow.onTap = { [weak self] in self?.onLocationTap?() } timeRow.onTap = { [weak self] in self?.onTimeTap?() } } var remarkText: String { remarkRow.textField?.text ?? "" } var remarkTextField: UITextField? { remarkRow.textField } override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear contentView.backgroundColor = .clear clipsToBounds = false contentView.clipsToBounds = false setupViews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func prepareForReuse() { super.prepareForReuse() disposeBag = DisposeBag() onLocationTap = nil onTimeTap = nil onDelete = nil onDragGesture = nil } private func setupViews() { // 先加虚线,再加圆点盖在上面 contentView.addSubview(dashLineView) contentView.addSubview(markerView) contentView.addSubview(middleDot) contentView.addSubview(cardView) markerView.addSubview(markerLab) cardView.addSubview(locationRow) cardView.addSubview(timeRow) cardView.addSubview(remarkRow) cardView.addSubview(dragBtn) cardView.addSubview(deleteBtn) markerView.layer.cornerRadius = ScheduleTimelineMetric.markerSize / 2 markerView.clipsToBounds = true markerLab.textColor = .white markerLab.font = FontManager.youSheBiaoTiHei(11) markerLab.textAlignment = .center middleDot.backgroundColor = UIColor(hexStr: "#293445") middleDot.layer.cornerRadius = 5 let middleCenter = UIView() middleCenter.backgroundColor = .white middleCenter.cornerRadius = 2 middleDot.addSubview(middleCenter) middleCenter.layoutChain .centerX() .centerY() .width(4) .heightToWidth(1) cardView.backgroundColor = .white cardView.layer.cornerRadius = 16 dragBtn.setImage(UIImage(named: "Schedule/drag")?.withRenderingMode(.alwaysOriginal), for: .normal) dragBtn.extendEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) deleteBtn.setImage(UIImage(named: "Schedule/delete")?.withRenderingMode(.alwaysOriginal), for: .normal) deleteBtn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) deleteBtn.addTarget(self, action: #selector(tapDelete), for: .touchUpInside) let press = UILongPressGestureRecognizer(target: self, action: #selector(handleDragPress(_:))) press.minimumPressDuration = 0.12 dragBtn.addGestureRecognizer(press) dragPress = press dashLineView.translatesAutoresizingMaskIntoConstraints = false markerView.translatesAutoresizingMaskIntoConstraints = false let topToContent = dashLineView.topAnchor.constraint(equalTo: contentView.topAnchor) let topToMarker = dashLineView.topAnchor.constraint(equalTo: markerView.centerYAnchor) let bottomToContent = dashLineView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) let bottomToMarker = dashLineView.bottomAnchor.constraint(equalTo: markerView.centerYAnchor) dashTopToContent = topToContent dashTopToMarkerCenter = topToMarker dashBottomToContent = bottomToContent dashBottomToMarkerCenter = bottomToMarker // 默认中间点:贯穿;configure 时再按 role 切换 topToMarker.isActive = false bottomToMarker.isActive = false NSLayoutConstraint.activate([ topToContent, bottomToContent, dashLineView.centerXAnchor.constraint(equalTo: contentView.leftAnchor, constant: ScheduleTimelineMetric.centerX), dashLineView.widthAnchor.constraint(equalToConstant: 1), markerView.centerXAnchor.constraint(equalTo: contentView.leftAnchor, constant: ScheduleTimelineMetric.centerX), markerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), markerView.widthAnchor.constraint(equalToConstant: ScheduleTimelineMetric.markerSize), markerView.heightAnchor.constraint(equalToConstant: ScheduleTimelineMetric.markerSize) ]) markerLab.layoutChain.edges() middleDot.layoutChain .centerX(markerView) .centerY(markerView) .width(10) .height(10) cardView.layoutChain .top(8) .left(50) .right(16) .bottom(8) // 上:时间,中:地点,下:备注 timeRow.layoutChain .top(10) .left(10) .right(10) .height(36) locationRow.translatesAutoresizingMaskIntoConstraints = false remarkRow.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ locationRow.topAnchor.constraint(equalTo: timeRow.bottomAnchor, constant: 8), locationRow.leftAnchor.constraint(equalTo: cardView.leftAnchor, constant: 10), locationRow.heightAnchor.constraint(equalToConstant: 36), remarkRow.topAnchor.constraint(equalTo: locationRow.bottomAnchor, constant: 8), remarkRow.leftAnchor.constraint(equalTo: cardView.leftAnchor, constant: 10), remarkRow.heightAnchor.constraint(equalToConstant: 36), remarkRow.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -10) ]) let locationRight = locationRow.rightAnchor.constraint(equalTo: cardView.rightAnchor, constant: -10) let remarkRight = remarkRow.rightAnchor.constraint(equalTo: cardView.rightAnchor, constant: -10) locationRight.isActive = true remarkRight.isActive = true locationRowRightConstraint = locationRight remarkRowRightConstraint = remarkRight dragBtn.layoutChain .right(10) .centerY(locationRow) .width(22) .height(22) deleteBtn.layoutChain .right(12) .centerY(remarkRow) .width(18) .height(18) } @objc private func tapDelete() { onDelete?() } @objc private func handleDragPress(_ gesture: UILongPressGestureRecognizer) { onDragGesture?(gesture) } } // MARK: - ScheduleAddPointCell final class ScheduleAddPointCell: UITableViewCell { static let reuseId = "ScheduleAddPointCell" var onAdd: (() -> Void)? private let dashLineView = ScheduleDashLineView() private let addBtn = UIButton(type: .custom) override init(style: CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear contentView.backgroundColor = .clear clipsToBounds = false contentView.clipsToBounds = false setup() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setup() { contentView.addSubview(dashLineView) contentView.addSubview(addBtn) addBtn.setTitle(" 添加行程", for: .normal) addBtn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) addBtn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium) addBtn.setImage(UIImage(named: "Schedule/add")?.withRenderingMode(.alwaysOriginal), for: .normal) addBtn.backgroundColor = UIColor(hexStr: "#F6FBFF") addBtn.layer.cornerRadius = 12 addBtn.addTarget(self, action: #selector(tapAdd), for: .touchUpInside) dashLineView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ dashLineView.topAnchor.constraint(equalTo: contentView.topAnchor), dashLineView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), dashLineView.centerXAnchor.constraint(equalTo: contentView.leftAnchor, constant: ScheduleTimelineMetric.centerX), dashLineView.widthAnchor.constraint(equalToConstant: 1) ]) addBtn.layoutChain .top(8) .left(50) .right(16) .height(44) .bottom(8) } private var dashBorder: CAShapeLayer? override func layoutSubviews() { super.layoutSubviews() dashBorder?.removeFromSuperlayer() let border = CAShapeLayer() border.strokeColor = UIColor(hexStr: "#16B3FF").cgColor border.fillColor = nil border.lineDashPattern = [5, 4] border.lineWidth = 1 border.frame = addBtn.bounds border.path = UIBezierPath(roundedRect: addBtn.bounds, cornerRadius: 12).cgPath addBtn.layer.borderWidth = 0 addBtn.layer.addSublayer(border) dashBorder = border } @objc private func tapAdd() { onAdd?() } } // MARK: - Field row private final class ScheduleFieldRowView: UIView { var onTap: (() -> Void)? private(set) var textField: UITextField? private let iconView = UIImageView() private let label = UILabel() private let placeholder: String init(icon: UIImage?, placeholder: String, editable: Bool = false) { self.placeholder = placeholder super.init(frame: .zero) backgroundColor = .white layer.cornerRadius = 8 iconView.image = icon?.withRenderingMode(.alwaysOriginal) iconView.contentMode = .scaleAspectFit addSubview(iconView) iconView.layoutChain.left(10).centerY().width(14).height(14) let tfView = UIView() tfView.backgroundColor = UIColor(hexStr: "#FAFAFA") tfView.cornerRadius = 8 addSubview(tfView) tfView.layoutChain.leftToRightOfView(iconView, offset: 8).right(10).centerY().height(34) if editable { let tf = UITextField() tf.font = .systemFont(ofSize: 13, weight: .medium) tf.textColor = UIColor(hexStr: "#333333") tf.placeholder = placeholder tf.returnKeyType = .done tfView.addSubview(tf) tf.layoutChain.edgesHorzontal(10).edgesVertical() textField = tf } else { label.font = .systemFont(ofSize: 13, weight: .medium) label.textColor = UIColor(hexStr: "#999999") label.text = placeholder label.isUserInteractionEnabled = true tfView.addSubview(label) label.layoutChain.edgesHorzontal(10).centerY() let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap)) addGestureRecognizer(tap) } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setText(_ text: String?) { if let tf = textField { tf.text = text return } if let text, !text.isEmpty { label.text = text label.textColor = UIColor(hexStr: "#333333") } else { label.text = placeholder label.textColor = UIColor(hexStr: "#999999") } } @objc private func handleTap() { onTap?() } } // MARK: - Dash line final class ScheduleDashLineView: 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.maxX == 0 ? 0.5 : bounds.midX, y: bounds.height)) shapeLayer.path = path.cgPath layer.addSublayer(shapeLayer) } } /// 兼容旧命名 typealias DashLineView = ScheduleDashLineView final 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) } }