355 lines
14 KiB
Swift
355 lines
14 KiB
Swift
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)
|
||
])
|
||
}
|
||
}
|