435 lines
14 KiB
Swift
435 lines
14 KiB
Swift
//
|
|
// GroupItineraryView.swift
|
|
// QuickLocation
|
|
//
|
|
|
|
import UIKit
|
|
import RxSwift
|
|
import RxCocoa
|
|
import SwiftDate
|
|
|
|
/// 圈子 Tab 下的「行程」内容页
|
|
final class GroupItineraryView: UIView {
|
|
|
|
var disposeBag = DisposeBag()
|
|
|
|
var onSwitchGroup: (() -> Void)?
|
|
var onDeleteSchedule: ((ScheduleModel) -> Void)?
|
|
var onSelectSchedule: ((ScheduleModel) -> Void)?
|
|
|
|
func updateGroupName(_ name: String) {
|
|
groupNameLab.text = name.isEmpty ? " " : name
|
|
}
|
|
|
|
func reloadSchedules(_ list: [ScheduleModel]) {
|
|
scheduleSections = Self.groupByDay(list)
|
|
tableView.reloadData()
|
|
emptyLab.isHidden = !list.isEmpty
|
|
}
|
|
|
|
private var scheduleSections: [(dayText: String, dateText: String, items: [ScheduleModel])] = []
|
|
|
|
private func setupUI() {
|
|
addSubview(headerRow)
|
|
headerRow.addSubview(groupIcon)
|
|
headerRow.addSubview(groupNameLab)
|
|
headerRow.addSubview(changeGroupBtn)
|
|
addSubview(tableView)
|
|
addSubview(emptyLab)
|
|
addSubview(createBtn)
|
|
|
|
headerRow.layoutChain
|
|
.top(8)
|
|
.edgesHorzontal(16)
|
|
.height(28)
|
|
|
|
groupIcon.layoutChain
|
|
.left()
|
|
.centerY()
|
|
.width(22)
|
|
.height(22)
|
|
|
|
changeGroupBtn.layoutChain
|
|
.right()
|
|
.centerY()
|
|
.height(28)
|
|
|
|
groupNameLab.layoutChain
|
|
.leftToRightOfView(groupIcon, offset: 8)
|
|
.centerY()
|
|
.rightToLeftOfView(changeGroupBtn, offset: -8, relation: .lessThanOrEqual)
|
|
|
|
tableView.layoutChain
|
|
.topToBottomOfView(headerRow, offset: 12)
|
|
.edgesHorzontal()
|
|
.bottom()
|
|
|
|
emptyLab.layoutChain
|
|
.centerX()
|
|
.centerY(tableView)
|
|
|
|
createBtn.layoutChain
|
|
.bottom(kSafeBottomMargin + 102)
|
|
.right(10)
|
|
.width(60)
|
|
.heightToWidth(1)
|
|
}
|
|
|
|
private func setupRx() {
|
|
createBtn.rx.tap.subscribe(onNext: { _ in
|
|
AppRouter.push(Route.createSchedule)
|
|
}).disposed(by: disposeBag)
|
|
|
|
changeGroupBtn.rx.tap
|
|
.subscribe(onNext: { [weak self] in self?.onSwitchGroup?() })
|
|
.disposed(by: disposeBag)
|
|
}
|
|
|
|
private static func groupByDay(_ list: [ScheduleModel]) -> [(dayText: String, dateText: String, items: [ScheduleModel])] {
|
|
let sorted = list.sorted { $0.timestamp > $1.timestamp }
|
|
var map: [String: [ScheduleModel]] = [:]
|
|
var order: [String] = []
|
|
var dayDateMap: [String: (dayText: String, dateText: String)] = [:]
|
|
let fmt = DateFormatter()
|
|
fmt.locale = Locale(identifier: "zh_CN")
|
|
let calendar = Calendar.current
|
|
for item in sorted {
|
|
let date = Date(timeIntervalSince1970: TimeInterval(item.timestamp) / 1000)
|
|
fmt.dateFormat = "M月d日"
|
|
let dateText = fmt.string(from: date)
|
|
let dayText: String
|
|
if calendar.isDateInToday(date) {
|
|
dayText = "今天"
|
|
} else if calendar.isDateInYesterday(date) {
|
|
dayText = "昨天"
|
|
} else {
|
|
fmt.dateFormat = "EEEE"
|
|
dayText = fmt.string(from: date)
|
|
}
|
|
let key = "\(dayText) \(dateText)"
|
|
if map[key] == nil {
|
|
order.append(key)
|
|
dayDateMap[key] = (dayText, dateText)
|
|
map[key] = []
|
|
}
|
|
map[key]?.append(item)
|
|
}
|
|
return order.map { key in
|
|
let pair = dayDateMap[key] ?? ("", "")
|
|
return (pair.dayText, pair.dateText, map[key] ?? [])
|
|
}
|
|
}
|
|
|
|
lazy var headerRow: UIView = {
|
|
let v = UIView()
|
|
v.backgroundColor = .clear
|
|
return v
|
|
}()
|
|
|
|
lazy var groupIcon: UIImageView = {
|
|
let iv = UIImageView(image: UIImage(named: "Home/group_name_icon"))
|
|
iv.contentMode = .scaleAspectFit
|
|
return iv
|
|
}()
|
|
|
|
lazy var groupNameLab: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 16, weight: .bold)
|
|
label.textColor = UIColor(hexStr: "#353B4F")
|
|
label.text = " "
|
|
return label
|
|
}()
|
|
|
|
lazy var changeGroupBtn: UIButton = {
|
|
let btn = UIButton(type: .custom)
|
|
btn.setTitle("切换圈子 ", for: .normal)
|
|
btn.setTitleColor(UIColor(hexStr: "#353B4F"), for: .normal)
|
|
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
|
btn.setImage(UIImage(named: "Common/arrow_right"), for: .normal)
|
|
btn.semanticContentAttribute = .forceRightToLeft
|
|
return btn
|
|
}()
|
|
|
|
lazy var tableView: UITableView = {
|
|
let tv = UITableView(frame: .zero, style: .grouped)
|
|
tv.backgroundColor = UIColor(hexStr: "#FAFAFA")
|
|
tv.separatorStyle = .none
|
|
tv.showsVerticalScrollIndicator = false
|
|
tv.register(GroupItineraryCell.self)
|
|
tv.dataSource = self
|
|
tv.delegate = self
|
|
tv.estimatedRowHeight = 100
|
|
tv.rowHeight = UITableView.automaticDimension
|
|
tv.sectionHeaderHeight = 36
|
|
tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0)
|
|
if #available(iOS 15.0, *) {
|
|
tv.sectionHeaderTopPadding = 0
|
|
}
|
|
return tv
|
|
}()
|
|
|
|
lazy var emptyLab: UILabel = {
|
|
let label = UILabel()
|
|
label.text = "暂无行程"
|
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
|
label.textColor = UIColor(hexStr: "#999999")
|
|
label.isHidden = true
|
|
return label
|
|
}()
|
|
|
|
lazy var createBtn: UIButton = {
|
|
let btn = UIButton()
|
|
btn.setImage(UIImage(named: "Schedule/create"), for: .normal)
|
|
return btn
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = .clear
|
|
setupUI()
|
|
setupRx()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
// MARK: - UITableView
|
|
extension GroupItineraryView: UITableViewDataSource, UITableViewDelegate {
|
|
func numberOfSections(in tableView: UITableView) -> Int {
|
|
scheduleSections.count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
scheduleSections[section].items.count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
|
let header = UIView()
|
|
header.backgroundColor = .clear
|
|
let sectionData = scheduleSections[section]
|
|
let label = UILabel()
|
|
let color = UIColor(hexStr: "#353B4F")
|
|
let attr = NSMutableAttributedString()
|
|
attr.append(NSAttributedString(
|
|
string: sectionData.dayText,
|
|
attributes: [
|
|
.font: UIFont.systemFont(ofSize: 16, weight: .bold),
|
|
.foregroundColor: color
|
|
]
|
|
))
|
|
if !sectionData.dateText.isEmpty {
|
|
attr.append(NSAttributedString(
|
|
string: " \(sectionData.dateText)",
|
|
attributes: [
|
|
.font: UIFont.systemFont(ofSize: 13, weight: .medium),
|
|
.foregroundColor: color
|
|
]
|
|
))
|
|
}
|
|
label.attributedText = attr
|
|
header.addSubview(label)
|
|
label.layoutChain.left(16).centerY()
|
|
return header
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
|
.leastNormalMagnitude
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
|
UIView()
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
let cell: GroupItineraryCell = tableView.dequeueReusableCell(for: indexPath)
|
|
cell.configure(scheduleSections[indexPath.section].items[indexPath.row])
|
|
return cell
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
onSelectSchedule?(scheduleSections[indexPath.section].items[indexPath.row])
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView,
|
|
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
let model = scheduleSections[indexPath.section].items[indexPath.row]
|
|
let delete = UIContextualAction(style: .destructive, title: nil) { [weak self] _, _, done in
|
|
self?.onDeleteSchedule?(model)
|
|
done(true)
|
|
}
|
|
delete.image = UIImage(systemName: "trash")
|
|
delete.backgroundColor = UIColor(hexStr: "#FF5A5F")
|
|
return UISwipeActionsConfiguration(actions: [delete])
|
|
}
|
|
}
|
|
|
|
// MARK: - GroupItineraryCell
|
|
final class GroupItineraryCell: UITableViewCell {
|
|
|
|
func configure(_ model: ScheduleModel) {
|
|
avatarImg.image = model.userIcon
|
|
nameLab.text = model.nick_name
|
|
let count = max(model.points.count, 0)
|
|
placeCountLab.text = "\(count)"
|
|
let date = Date(timeIntervalSince1970: TimeInterval(model.timestamp) / 1000)
|
|
let ongoing = Calendar.current.isDateInToday(date) || date > Date()
|
|
statusLab.text = ongoing ? "进行中" : "已完成"
|
|
statusLab.textColor = ongoing ? UIColor(hexStr: "#00B0C6") : UIColor(hexStr: "#999999")
|
|
statusLab.backgroundColor = ongoing
|
|
? UIColor(hexStr: "#DCFCFF")
|
|
: UIColor(hexStr: "#F0F0F0")
|
|
|
|
let tag = model.groups.first?.group_name ?? ""
|
|
tagLab.text = tag.isEmpty ? nil : " \(tag) "
|
|
tagLab.isHidden = tag.isEmpty
|
|
setNeedsLayout()
|
|
}
|
|
|
|
override init(style: CellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
selectionStyle = .none
|
|
backgroundColor = .clear
|
|
contentView.backgroundColor = .clear
|
|
|
|
contentView.addSubview(cardView)
|
|
cardView.addSubview(avatarImg)
|
|
cardView.addSubview(nameLab)
|
|
cardView.addSubview(statusLab)
|
|
cardView.addSubview(summaryLab)
|
|
cardView.addSubview(placeCountLab)
|
|
cardView.addSubview(summaryTailLab)
|
|
cardView.addSubview(tagLab)
|
|
|
|
cardView.layoutChain
|
|
.edgesVertical(6)
|
|
.edgesHorzontal(16)
|
|
|
|
avatarImg.layoutChain
|
|
.top(14)
|
|
.left(14)
|
|
.width(36)
|
|
.height(36)
|
|
|
|
nameLab.layoutChain
|
|
.leftToRightOfView(avatarImg, offset: 10)
|
|
.centerY(avatarImg)
|
|
|
|
statusLab.layoutChain
|
|
.right(14)
|
|
.centerY(avatarImg)
|
|
.height(22)
|
|
|
|
summaryLab.layoutChain
|
|
.topToBottomOfView(avatarImg, offset: 12)
|
|
.left(14)
|
|
|
|
placeCountLab.layoutChain
|
|
.leftToRightOfView(summaryLab, offset: 4)
|
|
.centerY(summaryLab)
|
|
.width(22)
|
|
.height(22)
|
|
|
|
summaryTailLab.layoutChain
|
|
.leftToRightOfView(placeCountLab, offset: 4)
|
|
.centerY(summaryLab)
|
|
|
|
tagLab.layoutChain
|
|
.topToBottomOfView(summaryLab, offset: 12)
|
|
.right(14)
|
|
.bottom(14)
|
|
.height(20)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private lazy var cardView: UIView = {
|
|
let v = UIView()
|
|
v.backgroundColor = .white
|
|
v.cornerRadius = 16
|
|
return v
|
|
}()
|
|
|
|
private lazy var avatarImg: UIImageView = {
|
|
let iv = UIImageView()
|
|
iv.contentMode = .scaleAspectFill
|
|
iv.cornerRadius = 10
|
|
iv.clipsToBounds = true
|
|
return iv
|
|
}()
|
|
|
|
private lazy var nameLab: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 14, weight: .bold)
|
|
label.textColor = UIColor(hexStr: "#353B4F")
|
|
return label
|
|
}()
|
|
|
|
private lazy var statusLab: PaddingLabel = {
|
|
let label = PaddingLabel()
|
|
label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
label.textAlignment = .center
|
|
label.cornerRadius = 4
|
|
label.clipsToBounds = true
|
|
label.insets = UIEdgeInsets(top: 2, left: 8, bottom: 2, right: 8)
|
|
label.borderWidth = 1
|
|
label.borderColor = UIColor(hexStr: "#84E2ED")
|
|
return label
|
|
}()
|
|
|
|
private lazy var summaryLab: UILabel = {
|
|
let label = UILabel()
|
|
label.text = "今天停留了"
|
|
label.font = .systemFont(ofSize: 16, weight: .bold)
|
|
label.textColor = UIColor(hexStr: "#353B4F")
|
|
return label
|
|
}()
|
|
|
|
private lazy var placeCountLab: UILabel = {
|
|
let label = UILabel()
|
|
label.font = FontManager.youSheBiaoTiHei(14)
|
|
label.textColor = UIColor(hexStr: "#58EDFF")
|
|
label.textAlignment = .center
|
|
label.backgroundColor = UIColor(hexStr: "#293445")
|
|
label.cornerRadius = 6
|
|
label.clipsToBounds = true
|
|
return label
|
|
}()
|
|
|
|
private lazy var summaryTailLab: UILabel = {
|
|
let label = UILabel()
|
|
label.text = "个地方"
|
|
label.font = .systemFont(ofSize: 16, weight: .bold)
|
|
label.textColor = UIColor(hexStr: "#353B4F")
|
|
return label
|
|
}()
|
|
|
|
private lazy var tagLab: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .systemFont(ofSize: 10, weight: .medium)
|
|
label.textColor = UIColor(hexStr: "#FF8A3D")
|
|
label.backgroundColor = UIColor(hexStr: "#FFE8D6")
|
|
label.cornerRadius = 4
|
|
label.clipsToBounds = true
|
|
label.textAlignment = .center
|
|
return label
|
|
}()
|
|
}
|
|
|
|
private final class PaddingLabel: UILabel {
|
|
var insets = UIEdgeInsets.zero
|
|
|
|
override func drawText(in rect: CGRect) {
|
|
super.drawText(in: rect.inset(by: insets))
|
|
}
|
|
|
|
override var intrinsicContentSize: CGSize {
|
|
let size = super.intrinsicContentSize
|
|
return CGSize(width: size.width + insets.left + insets.right,
|
|
height: size.height + insets.top + insets.bottom)
|
|
}
|
|
}
|