MOLineUserListView.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. //
  2. // MOLineUserListView.swift
  3. // MiMoLive
  4. //
  5. // Created by OneeChan on 2025/9/23.
  6. //
  7. import Foundation
  8. import UIKit
  9. protocol MOLineUserListViewDelegate: AnyObject {
  10. func onUserListViewClickSearch(view: MOLineUserListView, type: LineType)
  11. func onUserListViewClickMatch(view: MOLineUserListView, type: LineType)
  12. }
  13. class MOLineUserListView: UIView {
  14. private let curType: LineType
  15. private var userList: [MOLivePkLinkInviteVO] = []
  16. private var nextTag = ""
  17. private let userTitle = UILabel()
  18. private let noMoreDataView = MONoMoreDataView()
  19. private let tableView = UITableView()
  20. private weak var delegate: MOLineUserListViewDelegate?
  21. init(type: LineType, _ delegate: MOLineUserListViewDelegate?) {
  22. self.curType = type
  23. self.delegate = delegate
  24. super.init(frame: .zero)
  25. setupViews()
  26. reload()
  27. }
  28. required init?(coder: NSCoder) {
  29. fatalError("init(coder:) has not been implemented")
  30. }
  31. }
  32. extension MOLineUserListView {
  33. private func reload() {
  34. MOLineViewModel.loadLineMatchUsers { [weak self] list, hasMore in
  35. guard let self else { return }
  36. self.tableView.mj_header?.endRefreshing()
  37. guard let list else { return }
  38. self.noMoreDataView.isHaveData = !list.list.isEmpty
  39. self.userList = list.list
  40. self.nextTag = list.next
  41. if !hasMore {
  42. self.tableView.mj_footer?.endRefreshingWithNoMoreData()
  43. }
  44. self.tableView.reloadData()
  45. self.updateUserCountText()
  46. }
  47. }
  48. @objc
  49. private func loadNext() {
  50. guard !self.nextTag.isEmpty else {
  51. self.tableView.mj_footer?.endRefreshing()
  52. return
  53. }
  54. MOLineViewModel.loadLineMatchUsers(self.nextTag) { [weak self] list, hasMore in
  55. guard let self else { return }
  56. self.tableView.mj_footer?.endRefreshing()
  57. guard let list else { return }
  58. self.userList.append(contentsOf: list.list)
  59. self.nextTag = list.next
  60. if !hasMore {
  61. self.tableView.mj_footer?.endRefreshingWithNoMoreData()
  62. }
  63. self.tableView.reloadData()
  64. self.updateUserCountText()
  65. }
  66. }
  67. }
  68. extension MOLineUserListView {
  69. @objc
  70. private func handleSearchClick() {
  71. delegate?.onUserListViewClickSearch(view: self, type: curType)
  72. }
  73. @objc
  74. private func handleMatchClick() {
  75. delegate?.onUserListViewClickMatch(view: self, type: curType)
  76. }
  77. }
  78. extension MOLineUserListView: JXCategoryListContentViewDelegate {
  79. func listView() -> UIView! {
  80. self
  81. }
  82. }
  83. extension MOLineUserListView: UITableViewDataSource {
  84. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  85. userList.count
  86. }
  87. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  88. let cell = tableView.dequeueReusableCell(withIdentifier: MOLineUserListCell.className())
  89. if indexPath.row < userList.count, let cell = cell as? MOLineUserListCell {
  90. cell.updateContent(MOLineUserListItemData(curType: curType, item: userList[indexPath.row]))
  91. }
  92. return cell!
  93. }
  94. }
  95. extension MOLineUserListView: UITableViewDelegate {
  96. func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  97. 64
  98. }
  99. }
  100. extension MOLineUserListView {
  101. private func updateUserCountText() {
  102. let text: String = curType == .line ? .init(key: "C70054", userList.count) : .init(key: "C70062", userList.count)
  103. let attrStr = NSMutableAttributedString(string: text)
  104. let range = (attrStr.string as NSString).range(of: "\(userList.count)")
  105. attrStr.addAttributes([.font: UIFont.systemFont(ofSize: 16, weight: .semibold),
  106. .foregroundColor: UIColor.init(hex: "#4363FF")], range: range)
  107. userTitle.attributedText = attrStr
  108. }
  109. private func setupViews() {
  110. let search = buildSearch()
  111. addSubview(search)
  112. search.snp.makeConstraints { make in
  113. make.leading.equalToSuperview().offset(12)
  114. make.trailing.equalToSuperview().offset(-12)
  115. make.top.equalToSuperview().offset(16)
  116. make.height.equalTo(36)
  117. }
  118. let match = curType == .line ? buildLineMatch() : buildPkMatch()
  119. addSubview(match)
  120. match.snp.makeConstraints { make in
  121. make.leading.equalToSuperview().offset(12)
  122. make.trailing.equalToSuperview().offset(-12)
  123. make.top.equalTo(search.snp.bottom).offset(15)
  124. }
  125. let list = buildList()
  126. addSubview(list)
  127. list.snp.makeConstraints { make in
  128. make.leading.equalToSuperview()
  129. make.trailing.equalToSuperview()
  130. make.bottom.equalToSuperview()
  131. make.top.equalTo(match.snp.bottom).offset(19)
  132. }
  133. }
  134. private func buildSearch() -> UIView {
  135. let container = UIView()
  136. container.layer.cornerRadius = 8
  137. container.backgroundColor = .init(hex: "#F3F4FA")
  138. let search = UIImageView()
  139. search.image = UIImage(named: "icon_search")
  140. search.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  141. container.addSubview(search)
  142. search.snp.makeConstraints { make in
  143. make.leading.equalToSuperview().offset(12)
  144. make.centerY.equalToSuperview()
  145. }
  146. let hint = UILabel()
  147. hint.text = .init(key: "mimo_contact_search_hint")
  148. hint.font = .poppinsRegularFont(14)
  149. hint.textColor = .init(hex: "#878A99", alpha: 0.5)
  150. container.addSubview(hint)
  151. hint.snp.makeConstraints { make in
  152. make.leading.equalTo(search.snp.trailing).offset(6)
  153. make.trailing.equalToSuperview().offset(-6)
  154. make.centerY.equalToSuperview()
  155. }
  156. let button = UIButton()
  157. button.addTarget(self, action: #selector(handleSearchClick), for: .touchUpInside)
  158. container.addSubview(button)
  159. button.snp.makeConstraints { make in
  160. make.edges.equalToSuperview()
  161. }
  162. return container
  163. }
  164. private func buildLineMatch() -> UIView {
  165. let container = UIView()
  166. let bg = UIImageView()
  167. bg.image = UIImage(named: "icon_line_match_bg")
  168. container.addSubview(bg)
  169. bg.snp.makeConstraints { make in
  170. make.edges.equalToSuperview()
  171. }
  172. let match = UIImageView()
  173. let path = Bundle.main.path(forResource: "icon_line_match", ofType: "webp")
  174. if let path {
  175. match.sd_setImage(with: URL(fileURLWithPath: path))
  176. } else {
  177. match.isHidden = true
  178. }
  179. container.addSubview(match)
  180. match.snp.makeConstraints { make in
  181. make.top.equalToSuperview().offset(4)
  182. make.trailing.equalToSuperview().offset(-4)
  183. make.width.equalTo(90)
  184. make.height.equalTo(80)
  185. }
  186. let title = UILabel()
  187. title.text = .init(key: "C70051")
  188. title.font = .poppinsBoldFont(20)
  189. title.textColor = .init(hex: "#141924")
  190. container.addSubview(title)
  191. title.snp.makeConstraints { make in
  192. make.leading.equalToSuperview().offset(13)
  193. make.top.equalToSuperview().offset(13)
  194. }
  195. let text: String = .init(key: "C70052", 15)
  196. let attrStr = NSMutableAttributedString(string: text)
  197. let range = (attrStr.string as NSString).range(of: "15s")
  198. attrStr.addAttributes([.foregroundColor: UIColor.init(hex: "#4363FF")], range: range)
  199. let desc = UILabel()
  200. desc.font = .poppinsRegularFont(12)
  201. desc.textColor = .init(hex: "#5C5E66")
  202. desc.numberOfLines = 0
  203. desc.attributedText = attrStr
  204. container.addSubview(desc)
  205. desc.snp.makeConstraints { make in
  206. make.leading.equalToSuperview().offset(13)
  207. make.top.equalTo(title.snp.bottom).offset(6)
  208. make.trailing.equalTo(match.snp.leading).offset(-13)
  209. }
  210. let buttonText: String = .init(key: "C70053")
  211. let buttonAttrStr = NSMutableAttributedString(string: buttonText,
  212. attributes: [.foregroundColor: UIColor.white,
  213. .font: UIFont.poppinsSemiBoldFont(13)])
  214. let randomRange = (buttonAttrStr.string as NSString).range(of: "(Random)")
  215. buttonAttrStr.addAttributes([.foregroundColor: UIColor.white.withAlphaComponent(0.81),
  216. .font: UIFont.poppinsRegularFont(12)], range: randomRange)
  217. let matchButton = UIButton()
  218. matchButton.layer.cornerRadius = 12
  219. matchButton.clipsToBounds = true
  220. matchButton.setAttributedTitle(buttonAttrStr, for: .normal)
  221. matchButton.setBackgroundImage(commonGradientBg, for: .normal)
  222. matchButton.addTarget(self, action: #selector(handleMatchClick), for: .touchUpInside)
  223. container.addSubview(matchButton)
  224. matchButton.snp.makeConstraints { make in
  225. make.leading.equalToSuperview().offset(13)
  226. make.trailing.equalToSuperview().offset(-13)
  227. make.bottom.equalToSuperview().offset(-13)
  228. make.height.equalTo(40)
  229. }
  230. return container
  231. }
  232. private func buildPkMatch() -> UIView {
  233. let container = UIView()
  234. let bg = UIImageView()
  235. bg.image = UIImage(named: "icon_line_match_bg")
  236. container.addSubview(bg)
  237. bg.snp.makeConstraints { make in
  238. make.edges.equalToSuperview()
  239. }
  240. let match = UIImageView()
  241. let path = Bundle.main.path(forResource: "icon_line_pk_match", ofType: "webp")
  242. if let path {
  243. match.sd_setImage(with: URL(fileURLWithPath: path))
  244. } else {
  245. match.isHidden = true
  246. }
  247. container.addSubview(match)
  248. match.snp.makeConstraints { make in
  249. make.top.equalToSuperview().offset(5)
  250. make.trailing.equalToSuperview().offset(-5)
  251. make.width.equalTo(80)
  252. make.height.equalTo(76)
  253. }
  254. let pk = UIImageView()
  255. pk.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  256. pk.image = .init(named: "icon_line_pk")
  257. container.addSubview(pk)
  258. pk.snp.makeConstraints { make in
  259. make.centerY.equalTo(match)
  260. make.trailing.equalTo(match.snp.leading).offset(-3)
  261. }
  262. let avatar = UIImageView()
  263. avatar.layer.cornerRadius = 18
  264. avatar.layer.borderColor = UIColor.white.cgColor
  265. avatar.layer.borderWidth = 1
  266. avatar.clipsToBounds = true
  267. if let myAvatar = UserDefaults.avatar {
  268. avatar.sd_setImage(with: URL(string: myAvatar))
  269. }
  270. container.addSubview(avatar)
  271. avatar.snp.makeConstraints { make in
  272. make.centerY.equalTo(match)
  273. make.trailing.equalTo(pk.snp.leading).offset(-12)
  274. make.width.height.equalTo(36)
  275. }
  276. let textContainer = UIView()
  277. container.addSubview(textContainer)
  278. textContainer.snp.makeConstraints { make in
  279. make.leading.equalToSuperview().offset(13)
  280. make.centerY.equalTo(match)
  281. make.trailing.equalTo(avatar.snp.leading).offset(-13)
  282. }
  283. let title = UILabel()
  284. title.text = .init(key: "C70060")
  285. title.font = .poppinsBoldFont(20)
  286. title.textColor = .init(hex: "#141924")
  287. textContainer.addSubview(title)
  288. title.snp.makeConstraints { make in
  289. make.leading.equalToSuperview()
  290. make.top.equalToSuperview()
  291. make.trailing.equalToSuperview()
  292. }
  293. let text: String = .init(key: "C70061", 15)
  294. let attrStr = NSMutableAttributedString(string: text)
  295. let range = (attrStr.string as NSString).range(of: "15s")
  296. attrStr.addAttributes([.foregroundColor: UIColor.init(hex: "#4363FF")], range: range)
  297. let desc = UILabel()
  298. desc.font = .poppinsRegularFont(12)
  299. desc.textColor = .init(hex: "#5C5E66")
  300. desc.numberOfLines = 0
  301. desc.attributedText = attrStr
  302. textContainer.addSubview(desc)
  303. desc.snp.makeConstraints { make in
  304. make.leading.equalToSuperview()
  305. make.top.equalTo(title.snp.bottom).offset(6)
  306. make.trailing.equalToSuperview()
  307. make.bottom.equalToSuperview()
  308. }
  309. let matchButton = UIButton()
  310. matchButton.setTitle(.init(key: "C70015"), for: .normal)
  311. matchButton.setTitleColor(.white, for: .normal)
  312. matchButton.titleLabel?.font = .poppinsSemiBoldFont(13)
  313. matchButton.layer.cornerRadius = 12
  314. matchButton.clipsToBounds = true
  315. matchButton.setBackgroundImage(commonGradientBg, for: .normal)
  316. matchButton.addTarget(self, action: #selector(handleMatchClick), for: .touchUpInside)
  317. container.addSubview(matchButton)
  318. matchButton.snp.makeConstraints { make in
  319. make.leading.equalToSuperview().offset(13)
  320. make.trailing.equalToSuperview().offset(-13)
  321. make.bottom.equalToSuperview().offset(-13)
  322. make.height.equalTo(40)
  323. }
  324. return container
  325. }
  326. private func buildList() -> UIView {
  327. let container = UIView()
  328. userTitle.text = ""
  329. userTitle.textColor = .init(hex: "#5C5E66")
  330. userTitle.font = .poppinsRegularFont(15)
  331. container.addSubview(userTitle)
  332. userTitle.snp.makeConstraints { make in
  333. make.leading.equalToSuperview().offset(12)
  334. make.top.equalToSuperview()
  335. }
  336. noMoreDataView.isHaveData = true
  337. noMoreDataView.tipLab.text = .init(key: "C70010")
  338. let header = MJRefreshNormalHeader { [weak self] in
  339. guard let self else { return }
  340. self.reload()
  341. }
  342. header.lastUpdatedTimeLabel?.isHidden = true
  343. header.stateLabel?.isHidden = true
  344. tableView.mj_header = header
  345. let footer = MJRefreshAutoNormalFooter(refreshingTarget: self, refreshingAction: #selector(loadNext))
  346. footer.setTitle("", for: .noMoreData)
  347. tableView.mj_footer = footer
  348. tableView.register(MOLineUserListCell.self, forCellReuseIdentifier: MOLineUserListCell.className())
  349. tableView.separatorColor = .clear
  350. tableView.delegate = self
  351. tableView.dataSource = self
  352. tableView.allowsSelection = false
  353. tableView.backgroundView = noMoreDataView
  354. container.addSubview(tableView)
  355. tableView.snp.makeConstraints { make in
  356. make.leading.equalToSuperview()
  357. make.trailing.equalToSuperview()
  358. make.bottom.equalToSuperview()
  359. make.top.equalTo(userTitle.snp.bottom)
  360. }
  361. return container
  362. }
  363. }
  364. //import SwiftUI
  365. //
  366. //struct MOLineUserListViewPreview: UIViewRepresentable {
  367. // func makeUIView(context: Context) -> some UIView {
  368. // let viewModel = MOLineViewModel()
  369. // let view = UIView()
  370. // view.backgroundColor = .black
  371. // let list = MOLineUserListView(viewModel, nil)
  372. // view.addSubview(list)
  373. // list.snp.makeConstraints { make in
  374. // make.leading.trailing.centerY.equalToSuperview()
  375. // }
  376. //
  377. // return view
  378. // }
  379. //
  380. // func updateUIView(_ uiView: UIViewType, context: Context) { }
  381. //}
  382. //
  383. //#Preview {
  384. // MOLineUserListViewPreview()
  385. //}