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