497 lines
17 KiB
Swift
497 lines
17 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 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)
|
||
}
|
||
}
|