// // 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 reloadMembers(_ members: [GroupMemberModel], selectedId: String, groupKey: String = "") { memberList = Self.sortedMemberList(members, groupKey: groupKey) // 保留有效选中;无效则用传入 selectedId → 自己 → 第一项(对齐首页) if memberList.contains(where: { $0.user_id == selectedMemberId }) { // keep selectedMemberId } else if memberList.contains(where: { $0.user_id == selectedId }) { selectedMemberId = selectedId } else if let me = memberList.first(where: { $0.user_id == AppContextManager.shared.userId }) { selectedMemberId = me.user_id } else { selectedMemberId = memberList.first?.user_id ?? "" } memberCV.reloadData() scrollToSelectedMember(animated: false) } func reloadSchedules(_ list: [ScheduleModel]) { scheduleSections = Self.groupByDay(list) tableView.reloadData() emptyLab.isHidden = !list.isEmpty } private var memberList: [GroupMemberModel] = [] private var selectedMemberId: String = "" private var scheduleSections: [(title: String, items: [ScheduleModel])] = [] /// 与 HomeViewModel / GroupMemberView2 一致:圈主 -> 自己 -> 在线;同优先级保持接口原始顺序 private static func sortedMemberList(_ list: [GroupMemberModel], groupKey: String) -> [GroupMemberModel] { list.enumerated() .sorted { lhs, rhs in let lhsPriority = memberSortPriority(lhs.element, groupKey: groupKey) let rhsPriority = memberSortPriority(rhs.element, groupKey: groupKey) if lhsPriority != rhsPriority { return lhsPriority < rhsPriority } return lhs.offset < rhs.offset } .map(\.element) } private static func memberSortPriority(_ member: GroupMemberModel, groupKey: String) -> Int { if !groupKey.isEmpty, groupKey.contains(member.user_id) { return 0 } if member.user_id == AppContextManager.shared.userId { return 1 } if member.is_online { return 2 } return 3 } private func scrollToSelectedMember(animated: Bool) { guard !selectedMemberId.isEmpty, let idx = memberList.firstIndex(where: { $0.user_id == selectedMemberId }) else { return } let indexPath = IndexPath(item: idx, section: 0) DispatchQueue.main.async { [weak self] in guard let self = self, self.memberCV.numberOfSections > 0, self.memberCV.numberOfItems(inSection: 0) > idx else { return } self.memberCV.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: animated) } } private func setupUI() { addSubview(headerRow) headerRow.addSubview(groupIcon) headerRow.addSubview(groupNameLab) headerRow.addSubview(changeGroupBtn) let memberView = UIView() memberView.backgroundColor = .white memberView.cornerRadius = 20 memberView.addSubview(memberCV) addSubview(memberView) memberView.layoutChain .topToBottomOfView(headerRow, offset: 12) .edgesHorzontal(15) .height(100) addSubview(tableView) addSubview(emptyLab) 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) memberCV.layoutChain .edges(all: 5, excludingEdge: .bottom) .bottom() tableView.layoutChain .topToBottomOfView(memberCV, offset: 8) .edgesHorzontal() .bottom() emptyLab.layoutChain .centerX() .centerY(tableView) } private func setupRx() { changeGroupBtn.rx.tap .subscribe(onNext: { [weak self] in self?.onSwitchGroup?() }) .disposed(by: disposeBag) } private static func groupByDay(_ list: [ScheduleModel]) -> [(title: String, items: [ScheduleModel])] { let sorted = list.sorted { $0.timestamp > $1.timestamp } var map: [String: [ScheduleModel]] = [:] var order: [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 day = fmt.string(from: date) let title: String if calendar.isDateInToday(date) { title = "今天 \(day)" } else if calendar.isDateInYesterday(date) { title = "昨天 \(day)" } else { fmt.dateFormat = "EEEE M月d日" title = fmt.string(from: date) } if map[title] == nil { order.append(title) map[title] = [] } map[title]?.append(item) } return order.map { ($0, map[$0] ?? []) } } 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 memberCV: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.itemSize = CGSize(width: 61, height: 95) layout.minimumLineSpacing = 15 layout.sectionInset = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) layout.scrollDirection = .horizontal let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) cv.backgroundColor = .clear cv.showsHorizontalScrollIndicator = false cv.register(GroupMemberListCell.self) cv.dataSource = self cv.delegate = self return cv }() lazy var tableView: UITableView = { let tv = UITableView(frame: .zero, style: .grouped) tv.backgroundColor = .clear 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 }() override init(frame: CGRect) { super.init(frame: frame) backgroundColor = UIColor(hexStr: "#FAFAFA") setupUI() setupRx() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } // MARK: - UICollectionView extension GroupItineraryView: UICollectionViewDataSource, UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { memberList.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath) let model = memberList[indexPath.item] cell.configure( model: model, isCurrentUser: model.user_id == AppContextManager.shared.userId, isSelected: model.user_id == selectedMemberId ) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { selectedMemberId = memberList[indexPath.item].user_id memberCV.reloadData() scrollToSelectedMember(animated: true) } } // 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 label = UILabel() label.text = scheduleSections[section].title label.font = .systemFont(ofSize: 13, weight: .semibold) label.textColor = UIColor(hexStr: "#353B4F") 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: "#16B3FF") : UIColor(hexStr: "#999999") statusLab.backgroundColor = ongoing ? UIColor(hexStr: "#E3F6FF") : 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 = 18 iv.clipsToBounds = true return iv }() private lazy var nameLab: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 14, weight: .semibold) label.textColor = UIColor(hexStr: "#353B4F") return label }() private lazy var statusLab: PaddingLabel = { let label = PaddingLabel() label.font = .systemFont(ofSize: 11, weight: .medium) label.textAlignment = .center label.cornerRadius = 6 label.clipsToBounds = true label.insets = UIEdgeInsets(top: 2, left: 8, bottom: 2, right: 8) return label }() private lazy var summaryLab: UILabel = { let label = UILabel() label.text = "今天停留了" label.font = .systemFont(ofSize: 14, weight: .medium) label.textColor = UIColor(hexStr: "#353B4F") return label }() private lazy var placeCountLab: UILabel = { let label = UILabel() label.font = FontManager.youSheBiaoTiHei(12) label.textColor = .white label.textAlignment = .center label.backgroundColor = UIColor(hexStr: "#353B4F") label.cornerRadius = 11 label.clipsToBounds = true return label }() private lazy var summaryTailLab: UILabel = { let label = UILabel() label.text = "个地方" label.font = .systemFont(ofSize: 14, weight: .medium) 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) } }