448 lines
14 KiB
Swift
448 lines
14 KiB
Swift
//
|
||
// CreateSchedulePopView.swift
|
||
// QuickLocation
|
||
//
|
||
// Created by 八条 on 2026/6/23.
|
||
//
|
||
|
||
import UIKit
|
||
import RxSwift
|
||
import RxCocoa
|
||
import RxDataSources
|
||
import BRPickerView
|
||
|
||
// MARK: - 行程点编辑模型
|
||
struct SchedulePointItem: IdentifiableType, Equatable {
|
||
let identity: String = UUID().uuidString
|
||
var locationName: String = ""
|
||
var address: String = ""
|
||
var expectedTime: Date?
|
||
var remark: String = ""
|
||
}
|
||
|
||
typealias SchedulePointSection = SectionModel<String, SchedulePointItem>
|
||
|
||
// MARK: - CreateSchedulePopView
|
||
|
||
class CreateSchedulePopView: UIView {
|
||
|
||
var disposeBag = DisposeBag()
|
||
|
||
let pointsRelay = BehaviorRelay<[SchedulePointItem]>(value: [SchedulePointItem()])
|
||
|
||
// MARK: - Setup
|
||
|
||
private func setupRx() {
|
||
dateLab.rx.tapGesture.subscribe { _ in
|
||
let picker = BRDatePickerView(pickerMode: .YMD)
|
||
picker.minDate = Date()
|
||
picker.maxDate = Calendar.current.date(byAdding: .day, value: 6, to: Date())
|
||
let style = BRPickerStyle()
|
||
style.selectRowTextColor = UIColor(hexStr: "#16B3FF")
|
||
picker.pickerStyle = style
|
||
picker.resultBlock = { [weak self] selectDate, value in
|
||
guard let date = selectDate else { return }
|
||
let fmt = DateFormatter()
|
||
fmt.dateFormat = "yyyy年MM月dd日"
|
||
self?.dateLab.text = fmt.string(from: date)
|
||
}
|
||
picker.show()
|
||
}.disposed(by: disposeBag)
|
||
|
||
// 新增行程点
|
||
addBtn.rx.tap
|
||
.subscribe(onNext: { [weak self] _ in
|
||
guard let self = self else { return }
|
||
var list = self.pointsRelay.value
|
||
list.append(SchedulePointItem(
|
||
locationName: "",
|
||
address: "",
|
||
expectedTime: nil,
|
||
remark: ""
|
||
))
|
||
self.pointsRelay.accept(list)
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
// 绑定 tableView
|
||
pointsRelay
|
||
.observe(on: MainScheduler.asyncInstance)
|
||
.map { [SchedulePointSection(model: "", items: $0)] }
|
||
.bind(to: tableView.rx.items(dataSource: dataSource))
|
||
.disposed(by: disposeBag)
|
||
|
||
// 动态更新 tableView 高度
|
||
pointsRelay
|
||
.subscribe(onNext: { [weak self] items in
|
||
guard let self = self else { return }
|
||
let rowHeight: CGFloat = 122
|
||
var h = CGFloat(items.count) * rowHeight
|
||
h = max(h, rowHeight)
|
||
self.tableView.layoutChain.height(h)
|
||
})
|
||
.disposed(by: disposeBag)
|
||
}
|
||
|
||
private lazy var dataSource: RxTableViewSectionedReloadDataSource<SchedulePointSection> = {
|
||
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?.pointsRelay.value.count ?? 0,
|
||
onDelete: { [weak self] in
|
||
guard let self = self else { return }
|
||
var list = self.pointsRelay.value
|
||
guard indexPath.row < list.count else { return }
|
||
list.remove(at: indexPath.row)
|
||
self.pointsRelay.accept(list)
|
||
})
|
||
return cell
|
||
})
|
||
}()
|
||
|
||
// MARK: - UI
|
||
|
||
private func setupUI() {
|
||
addSubview(lineView)
|
||
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()
|
||
.height(22)
|
||
|
||
scrollView.layoutChain
|
||
.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
|
||
let titleLab = UILabel()
|
||
titleLab.text = "行程日期:"
|
||
titleLab.font = .systemFont(ofSize: 14, weight: .medium)
|
||
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||
view.addSubview(titleLab)
|
||
titleLab.layoutChain.left(15).centerY()
|
||
view.addSubview(dateLab)
|
||
dateLab.layoutChain.leftToRightOfView(titleLab).centerY()
|
||
return view
|
||
}()
|
||
|
||
lazy var dateLab: UILabel = {
|
||
let label = UILabel()
|
||
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||
label.textColor = UIColor(hexStr: "#16B3FF")
|
||
label.isUserInteractionEnabled = true
|
||
let fmt = DateFormatter()
|
||
fmt.dateFormat = "yyyy年MM月dd日"
|
||
label.text = fmt.string(from: Date())
|
||
return label
|
||
}()
|
||
|
||
lazy var detailView: UIView = {
|
||
let view = UIView()
|
||
view.backgroundColor = .clear
|
||
let titleLab = UILabel()
|
||
titleLab.text = "行程详情"
|
||
titleLab.font = .systemFont(ofSize: 14, weight: .medium)
|
||
titleLab.textColor = ThemeManager.shared.color.titleAuxColor
|
||
view.addSubview(titleLab)
|
||
titleLab.layoutChain.left(15).centerY()
|
||
view.addSubview(addBtn)
|
||
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)
|
||
btn.backgroundColor = .clear
|
||
btn.extendEdgeInsets = UIEdgeInsets(top: 30, left: 10, bottom: 10, right: 15)
|
||
return btn
|
||
}()
|
||
|
||
lazy var scrollView: UIScrollView = {
|
||
let view = UIScrollView()
|
||
view.backgroundColor = .white
|
||
view.showsHorizontalScrollIndicator = false
|
||
view.bounces = false
|
||
|
||
let contentView = UIView()
|
||
contentView.backgroundColor = .clear
|
||
view.addSubview(contentView)
|
||
contentView.layoutChain
|
||
.edges().widthToView(view)
|
||
|
||
contentView.addSubview(tableView)
|
||
tableView.layoutChain
|
||
.top()
|
||
.edgesHorzontal()
|
||
|
||
contentView.addSubview(createBtn)
|
||
createBtn.layoutChain
|
||
.topToBottomOfView(tableView, offset: 30)
|
||
.edgesHorzontal(30)
|
||
.height(50)
|
||
.bottom(kSafeBottomMargin + 10)
|
||
|
||
return view
|
||
}()
|
||
|
||
lazy var tableView: UITableView = {
|
||
let tv = UITableView(frame: .zero, style: .plain)
|
||
tv.backgroundColor = .clear
|
||
tv.separatorStyle = .none
|
||
tv.estimatedRowHeight = 122
|
||
tv.isScrollEnabled = false
|
||
tv.showsVerticalScrollIndicator = false
|
||
tv.bounces = false
|
||
tv.register(SchedulePointCell.self)
|
||
return tv
|
||
}()
|
||
|
||
lazy var createBtn: UIButton = {
|
||
let btn = UIButton(type: .custom)
|
||
btn.setTitle("立即创建", for: .normal)
|
||
btn.setTitleColor(.white, for: .normal)
|
||
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||
btn.cornerRadius = 25
|
||
return btn
|
||
}()
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .white
|
||
setupUI()
|
||
setupRx()
|
||
}
|
||
|
||
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 {
|
||
|
||
var disposeBag = DisposeBag()
|
||
|
||
func configure(item: SchedulePointItem, index: Int, total: Int, onDelete: @escaping () -> Void) {
|
||
disposeBag = DisposeBag()
|
||
indexLabel.text = "\(index + 1)"
|
||
locationLabel.text = item.locationName.isEmpty ? "点击选择地点" : item.locationName
|
||
|
||
if let expectedTime = item.expectedTime {
|
||
timeLabel.text = formatTime(expectedTime)
|
||
}
|
||
else {
|
||
timeLabel.text = "请选择到达时间"
|
||
}
|
||
|
||
// 左侧竖线:仅有一条时不显示虚线,首条顶部不画、末条底部不画
|
||
topDashView.isHidden = total <= 1 || index == 0
|
||
bottomDashView.isHidden = total <= 1 || index == total - 1
|
||
|
||
// 删除
|
||
deleteBtn.rx.tap
|
||
.subscribe(onNext: { onDelete() })
|
||
.disposed(by: disposeBag)
|
||
}
|
||
|
||
private func formatTime(_ date: Date) -> String {
|
||
let fmt = DateFormatter()
|
||
fmt.dateFormat = "HH:mm"
|
||
return fmt.string(from: date)
|
||
}
|
||
|
||
// 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") }
|
||
|
||
private func setupViews() {
|
||
contentView.addSubview(topDashView)
|
||
contentView.addSubview(pointIcon)
|
||
contentView.addSubview(bottomDashView)
|
||
|
||
contentView.addSubview(cardView)
|
||
cardView.addSubview(cardCornerView)
|
||
cardCornerView.addSubview(indexLabel)
|
||
cardCornerView.addSubview(locationLabel)
|
||
cardCornerView.addSubview(timeLabel)
|
||
cardCornerView.addSubview(remarkTF)
|
||
cardCornerView.addSubview(deleteBtn)
|
||
|
||
topDashView.layoutChain
|
||
.top().width(2)
|
||
|
||
pointIcon.layoutChain
|
||
.centerY()
|
||
.left(22)
|
||
.width(18).height(18)
|
||
|
||
topDashView.layoutChain
|
||
.centerX(pointIcon)
|
||
.bottomToTopOfView(pointIcon, offset: -10)
|
||
|
||
bottomDashView.layoutChain
|
||
.topToBottomOfView(pointIcon, offset: 10)
|
||
.centerX(pointIcon)
|
||
.width(2)
|
||
.bottom()
|
||
|
||
// 右侧卡片
|
||
cardView.layoutChain
|
||
.top(15)
|
||
.leftToRightOfView(pointIcon, offset: 20)
|
||
.right(15)
|
||
.height(92)
|
||
.bottom(15)
|
||
|
||
cardCornerView.layoutChain.edges()
|
||
|
||
indexLabel.layoutChain
|
||
.top().left()
|
||
.width(30).height(20)
|
||
|
||
locationLabel.layoutChain
|
||
.topToBottomOfView(indexLabel, offset: 10)
|
||
.left(10)
|
||
|
||
timeLabel.layoutChain
|
||
.centerY(locationLabel)
|
||
.leftToRightOfView(locationLabel, offset: 38)
|
||
|
||
deleteBtn.layoutChain
|
||
.bottom(15).right(15)
|
||
.width(14).height(14)
|
||
|
||
remarkTF.layoutChain
|
||
.topToBottomOfView(locationLabel, offset: 15)
|
||
.leftToView(locationLabel)
|
||
.rightToLeftOfView(deleteBtn, offset: -10)
|
||
}
|
||
|
||
// MARK: - Views
|
||
private let topDashView: UIView = {
|
||
let v = UIView()
|
||
v.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||
v.isHidden = true
|
||
return v
|
||
}()
|
||
|
||
private let pointIcon: UIImageView = {
|
||
let iv = UIImageView(image: UIImage(named: "Schedule/point"))
|
||
iv.contentMode = .scaleAspectFit
|
||
return iv
|
||
}()
|
||
|
||
private let bottomDashView: UIView = {
|
||
let v = UIView()
|
||
v.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||
v.isHidden = true
|
||
return v
|
||
}()
|
||
|
||
private let cardView: UIView = {
|
||
let view = UIView()
|
||
view.backgroundColor = .clear
|
||
view.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.05).cgColor
|
||
view.layer.shadowOffset = CGSize(width: 0, height: 0)
|
||
view.layer.shadowOpacity = 1
|
||
view.layer.shadowRadius = 8
|
||
return view
|
||
}()
|
||
|
||
lazy var cardCornerView: UIView = {
|
||
let view = UIView()
|
||
view.backgroundColor = .white
|
||
view.cornerRadius = 10
|
||
return view
|
||
}()
|
||
|
||
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
|
||
}()
|
||
|
||
private let locationLabel: UILabel = {
|
||
let l = UILabel()
|
||
l.font = .systemFont(ofSize: 14, weight: .medium)
|
||
l.textColor = UIColor(hexStr: "#333333")
|
||
l.lineBreakMode = .byTruncatingTail
|
||
return l
|
||
}()
|
||
|
||
private let timeLabel: UILabel = {
|
||
let l = UILabel()
|
||
l.font = .systemFont(ofSize: 12, weight: .medium)
|
||
l.textColor = UIColor(hexStr: "#333333")
|
||
l.isUserInteractionEnabled = true
|
||
return l
|
||
}()
|
||
|
||
lazy var remarkTF: UITextField = {
|
||
let tf = UITextField()
|
||
tf.font = .systemFont(ofSize: 12)
|
||
tf.placeholder = "在此添加备注..."
|
||
tf.returnKeyType = .done
|
||
return tf
|
||
}()
|
||
|
||
private let deleteBtn: UIButton = {
|
||
let btn = UIButton(type: .custom)
|
||
btn.setImage(UIImage(named: "Schedule/delete"), for: .normal)
|
||
btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 15, bottom: 15, right: 15)
|
||
return btn
|
||
}()
|
||
|
||
override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
cardCornerView.layoutIfNeeded()
|
||
indexLabel.setCornerRadius(corners: [.bottomRight], withCornerRadii: CGSize(width: 10, height: 10))
|
||
}
|
||
}
|