import UIKit import ObjectMapper import RxSwift struct SOSListResponse: BaseModelProtocol { var code: String? var message: String? var list: [SOSListItem] = [] init?(map: Map) {} mutating func mapping(map: Map) { code <- (map["code"], kIntTransformStr) message <- map["message"] if message == nil { message <- map["msg"] } list <- map["data"] if list.isEmpty { list <- map["data.list"] } } } struct SOSListItem: Mappable { var userId = "" var nickName = "" var headPic = "" var lastPosition = "" var groupKey = "" var groupName = "" var sosTime = "" init?(map: Map) {} mutating func mapping(map: Map) { userId <- (map["user_id"], kIntTransformStr) nickName <- (map["nick_name"], kIntTransformStr) headPic <- (map["head_pic"], kIntTransformStr) lastPosition <- (map["last_position"], kIntTransformStr) groupKey <- (map["group_key"], kIntTransformStr) groupName <- (map["group_name"], kIntTransformStr) sosTime <- (map["sos_time"], kIntTransformStr) } var displayAddress: String { let value = lastPosition.trimmed let parts = value.split(separator: ":", maxSplits: 2, omittingEmptySubsequences: false) guard parts.count == 3, Double(parts[0]) != nil, Double(parts[1]) != nil else { return value } let address = String(parts[2]).trimmed return address.isEmpty ? value : address } var displayTime: String { guard let date = parsedSOSDate else { return sosTime } return Self.displayDateFormatter.string(from: date) } private var parsedSOSDate: Date? { let value = sosTime.trimmed if let timestamp = TimeInterval(value) { return Date(timeIntervalSince1970: timestamp > 10_000_000_000 ? timestamp / 1000 : timestamp) } for formatter in Self.apiDateFormatters { if let date = formatter.date(from: value) { return date } } return ISO8601DateFormatter().date(from: value) } private static let apiDateFormatters: [DateFormatter] = [ "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm" ].map { format in let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = .current formatter.dateFormat = format return formatter } private static let displayDateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.locale = Locale(identifier: "zh_CN") formatter.timeZone = .current formatter.dateFormat = "M月d日 HH:mm" return formatter }() } final class SOSListViewController: BaseViewController { private var rootView: SOSListView! private var items: [SOSListItem] = [] private var isLoading = false override var isNavigationBarHidden: Bool { true } override func loadView() { rootView = SOSListView(frame: UIScreen.main.bounds) view = rootView } override func viewDidLoad() { super.viewDidLoad() rootView.tableView.dataSource = self rootView.tableView.delegate = self rootView.refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged) loadSOSList(showsLoading: true) } @objc private func refresh() { loadSOSList(showsLoading: false) } private func loadSOSList(showsLoading: Bool) { guard !isLoading else { return } isLoading = true if showsLoading { DLToast.showLoading() } SystemService.sosList() .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] response in guard let self else { return } self.finishLoading() guard response.code == "0" else { DLToast.showError(text: response.message ?? "获取求助列表失败") return } self.items = response.list self.rootView.showEmpty(self.items.isEmpty) self.rootView.tableView.reloadData() }, onError: { [weak self] error in self?.finishLoading() DLToast.showError(text: error.gatewayMessage ?? error.localizedDescription) }) .disposed(by: disposeBag) } private func finishLoading() { isLoading = false DLToast.dismiss() rootView.refreshControl.endRefreshing() } private func showLocation(for item: SOSListItem) { guard !item.groupKey.isEmpty, !item.lastPosition.isEmpty else { return } NotificationCenter.default.post( name: .ShowMemberLocationNotification, object: nil, userInfo: [ "last_position": item.lastPosition, "group_key": item.groupKey, "user_id": item.userId ] ) let mainTabBarController = tabBarController as? MainTabBarController navigationController?.popToRootViewController(animated: true) mainTabBarController?.selectTab(at: 0) } } extension SOSListViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: SOSListCell = tableView.dequeueReusableCell(for: indexPath) cell.configure(with: items[indexPath.row]) return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) showLocation(for: items[indexPath.row]) } } final class SOSListView: UIView { let tableView = UITableView(frame: .zero, style: .plain) let refreshControl = UIRefreshControl() private let navBackgroundView = UIImageView(image: UIImage(named: "Common/navBar_bg_2")) private let navView = BaseNavigationView(title: "紧急求助") private let emptyLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func showEmpty(_ isEmpty: Bool) { emptyLabel.isHidden = !isEmpty } private func setupUI() { backgroundColor = UIColor(hexStr: "#FAFAFA") navBackgroundView.contentMode = .scaleAspectFill navBackgroundView.clipsToBounds = true addSubview(navBackgroundView) addSubview(navView) tableView.backgroundColor = .clear tableView.separatorStyle = .none tableView.showsVerticalScrollIndicator = false tableView.estimatedRowHeight = 126 tableView.rowHeight = UITableView.automaticDimension tableView.contentInset = UIEdgeInsets(top: 14, left: 0, bottom: kSafeBottomMargin + 16, right: 0) tableView.register(SOSListCell.self) tableView.refreshControl = refreshControl addSubview(tableView) emptyLabel.text = "暂无紧急求助" emptyLabel.textColor = UIColor(hexStr: "#A7ABB2") emptyLabel.font = .systemFont(ofSize: 15) emptyLabel.textAlignment = .center emptyLabel.isHidden = true addSubview(emptyLabel) navBackgroundView.translatesAutoresizingMaskIntoConstraints = false navView.translatesAutoresizingMaskIntoConstraints = false tableView.translatesAutoresizingMaskIntoConstraints = false emptyLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ navBackgroundView.topAnchor.constraint(equalTo: topAnchor), navBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor), navBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor), navBackgroundView.heightAnchor.constraint(equalToConstant: max(kNaviHeight + 70, 150)), navView.topAnchor.constraint(equalTo: topAnchor), navView.leadingAnchor.constraint(equalTo: leadingAnchor), navView.trailingAnchor.constraint(equalTo: trailingAnchor), navView.heightAnchor.constraint(equalToConstant: kNaviHeight), tableView.topAnchor.constraint(equalTo: navView.bottomAnchor), tableView.leadingAnchor.constraint(equalTo: leadingAnchor), tableView.trailingAnchor.constraint(equalTo: trailingAnchor), tableView.bottomAnchor.constraint(equalTo: bottomAnchor), emptyLabel.centerXAnchor.constraint(equalTo: centerXAnchor), emptyLabel.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -20) ]) } } final class SOSListCell: UITableViewCell { private let cardView = UIView() private let avatarView = UIImageView() private let titleLabel = UILabel() private let addressLabel = UILabel() private let timeLabel = UILabel() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func prepareForReuse() { super.prepareForReuse() avatarView.image = nil } func configure(with item: SOSListItem) { avatarView.setHeadPic(item.headPic) titleLabel.attributedText = makeTitle(item) addressLabel.text = item.displayAddress.isEmpty ? "位置暂未获取" : item.displayAddress timeLabel.text = item.displayTime accessibilityLabel = [item.groupName, item.nickName, "紧急求助中", addressLabel.text, timeLabel.text] .compactMap { $0 } .filter { !$0.isEmpty } .joined(separator: ",") } private func makeTitle(_ item: SOSListItem) -> NSAttributedString { let groupName = item.groupName.isEmpty ? "当前" : item.groupName let nickName = item.nickName.isEmpty ? "圈子成员" : item.nickName let text = "\(groupName)圈子的 \(nickName) 紧急求助中" let result = NSMutableAttributedString( string: text, attributes: [ .font: UIFont.systemFont(ofSize: 16, weight: .regular), .foregroundColor: UIColor(hexStr: "#273246") ] ) let highlightColor = UIColor(hexStr: "#04AFFF") result.addAttribute(.foregroundColor, value: highlightColor, range: (text as NSString).range(of: groupName)) result.addAttribute(.foregroundColor, value: highlightColor, range: (text as NSString).range(of: nickName)) return result } private func setupUI() { backgroundColor = .clear selectionStyle = .none contentView.backgroundColor = .clear cardView.backgroundColor = .white cardView.layer.cornerRadius = 22 contentView.addSubview(cardView) avatarView.contentMode = .scaleAspectFill avatarView.clipsToBounds = true avatarView.layer.cornerRadius = 25 cardView.addSubview(avatarView) titleLabel.numberOfLines = 2 titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) cardView.addSubview(titleLabel) addressLabel.font = .systemFont(ofSize: 14) addressLabel.textColor = UIColor(hexStr: "#8D9198") addressLabel.numberOfLines = 2 cardView.addSubview(addressLabel) timeLabel.font = .systemFont(ofSize: 13) timeLabel.textColor = UIColor(hexStr: "#B6B9BE") cardView.addSubview(timeLabel) cardView.translatesAutoresizingMaskIntoConstraints = false avatarView.translatesAutoresizingMaskIntoConstraints = false titleLabel.translatesAutoresizingMaskIntoConstraints = false addressLabel.translatesAutoresizingMaskIntoConstraints = false timeLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ cardView.topAnchor.constraint(equalTo: contentView.topAnchor), cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15), cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -14), avatarView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 15), avatarView.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 27), avatarView.widthAnchor.constraint(equalToConstant: 50), avatarView.heightAnchor.constraint(equalToConstant: 50), titleLabel.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 22), titleLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 15), titleLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -15), addressLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), addressLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), addressLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), timeLabel.topAnchor.constraint(equalTo: addressLabel.bottomAnchor, constant: 7), timeLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), timeLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), timeLabel.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -16) ]) } }