// // GroupMemberView2.swift // QuickLocation // // Created by 八条 on 2026/8/4. // import UIKit import Network import CoreLocation import SwiftyUserDefaults class GroupMemberView2: UIView { private var navBarHeightConstraint: NSLayoutConstraint? private var todayTrackTopToInteraction: NSLayoutConstraint? private var todayTrackTopToMember: NSLayoutConstraint? private var currentMemberModel: GroupMemberModel? private var emojiEchoByUserId: [String: String] = [:] private var messageEchoByUserId: [String: String] = [:] private static let batteryNormalColor = UIColor(hexStr: "#47DD00") private static let batteryLowColor = UIColor(hexStr: "#F85A5D") private static let batteryDefaultTextColor = UIColor(hexStr: "#353B4F") /// 24pt 图标内腔可用填充宽度(去掉描边与右侧电极) private static let batteryFillMaxWidth: CGFloat = 18 /// 最大化时展开导航占位为 kNaviHeight,收起/dismiss 时为 0 func setNavBarExpanded(_ expanded: Bool) { navBarHeightConstraint?.constant = expanded ? kNaviHeight : 0 } func setupMemberInfo( _ model: GroupMemberModel, isOwner: Bool, phoneUsage: PhoneUsageTodayModel? ) { currentMemberModel = model memberNameLab.text = model.nick_name ownView.isHidden = !isOwner relationIconView.configure(relationIdx: model.extra.relation_idx) updateMoodBadge(model.mood) statusDotView.backgroundColor = UIColor(hexStr: model.is_online ? "#67EA76" : "#D8D8D8") statusLab.text = model.is_online ? "在线" : "离线" locationLab.text = model.lastLocation let isCurrentUser = model.user_id == AppContextManager.shared.userId updateDistance(for: model, isCurrentUser: isCurrentUser) applyPhoneUsage(phoneUsage) updateRealtimeInteractionVisibility(isCurrentUser: isCurrentUser) refreshRealtimeInteraction() } func updatePhoneUsage(_ phoneUsage: PhoneUsageTodayModel?, for userId: String) { guard currentMemberModel?.user_id == userId else { return } applyPhoneUsage(phoneUsage) } func applyEmojiEcho(name: String, for userId: String) { emojiEchoByUserId[userId] = name if currentMemberModel?.user_id == userId { refreshRealtimeInteraction() } } func applyMessageEcho(text: String, for userId: String) { messageEchoByUserId[userId] = text if currentMemberModel?.user_id == userId { refreshRealtimeInteraction() } } private func refreshRealtimeInteraction() { let userId = currentMemberModel?.user_id ?? "" applyEmojiDisplay(emojiEchoByUserId[userId]) applyMessageDisplay(messageEchoByUserId[userId]) } private func updateRealtimeInteractionVisibility(isCurrentUser: Bool) { let show = !isCurrentUser interactionView.isHidden = !show if show { todayTrackTopToMember?.isActive = false todayTrackTopToInteraction?.isActive = true } else { todayTrackTopToInteraction?.isActive = false todayTrackTopToMember?.isActive = true } } private func applyEmojiDisplay(_ name: String?) { if let name, let image = InteractionEmojiCell.pngImage(named: name) { emojiImageView.image = image emojiImageView.tintColor = nil return } let config = UIImage.SymbolConfiguration(pointSize: 36, weight: .regular) emojiImageView.image = UIImage(systemName: "face.smiling", withConfiguration: config) emojiImageView.tintColor = UIColor(hexStr: "#C8CDD6") } private func applyMessageDisplay(_ text: String?) { messageTextLab.text = text ?? "暂时没有消息记录" } private func updateDistance(for model: GroupMemberModel, isCurrentUser: Bool) { guard !isCurrentUser else { distanceView.isHidden = true return } guard let lat = Defaults[\.currentLatitude], let lon = Defaults[\.currentLongitude] else { distanceView.isHidden = true return } let meCoord = CLLocationCoordinate2D(latitude: lat, longitude: lon) let (otherCoord, _) = CircleMember.parsePosition(model.last_position) guard CLLocationCoordinate2DIsValid(meCoord), CLLocationCoordinate2DIsValid(otherCoord) else { distanceView.isHidden = true return } let meters = CLLocation(latitude: meCoord.latitude, longitude: meCoord.longitude) .distance(from: CLLocation(latitude: otherCoord.latitude, longitude: otherCoord.longitude)) distanceLab.text = Self.formatDistance(meters) distanceView.isHidden = false } private static func formatDistance(_ meters: CLLocationDistance) -> String { if meters < 1000 { return "\(Int(meters.rounded()))M" } return String(format: "%.1fKM", meters / 1000.0) } private func updateMoodBadge(_ mood: Int) { let image = MoodStatusCatalog.item(moodIndex: mood)?.pngImage moodImageView.image = image moodImageView.isHidden = image == nil } private func applyPhoneUsage(_ usage: PhoneUsageTodayModel?) { applyDeviceInfo(usage?.phoneInfo) phoneReportView.configure(with: Self.phoneReportPreview(from: usage)) applyStayPoints(usage?.stayPoints ?? []) } private func applyDeviceInfo(_ info: PhoneUsageInfoModel?) { let model = info?.model.trimmed ?? "" let hasModel = !model.isEmpty && model != "未知" phoneModelIcon.image = UIImage(named: hasModel ? "Home/member_phone" : "Home/member_phone_unknown") phoneModelLab.text = hasModel ? model : "***" phoneModelLab.textColor = Self.batteryDefaultTextColor let network = info?.network.trimmed ?? "" let hasNetwork = !network.isEmpty && network != "未知" networkIcon.image = UIImage(named: hasNetwork ? "Home/member_wifi" : "Home/member_network_unknown") networkLab.text = hasNetwork ? network : "***" networkLab.textColor = Self.batteryDefaultTextColor applyBatteryDisplay(percent: Self.percent(from: info?.battery)) let weather = info?.weather.trimmed ?? "" guard !weather.isEmpty else { applyUnknownWeather(text: "***") return } let snapshot = MemberWeatherService.snapshot(reportedText: weather) weatherIcon.image = UIImage(named: snapshot.assetName) weatherLab.text = snapshot.text } private func applyUnknownWeather(text: String) { weatherIcon.image = UIImage(named: "Home/member_weather_unknown") weatherLab.text = text } private func applyBatteryUnknown() { batteryIcon.image = UIImage(named: "Home/member_battery_unknown") batteryFillView.isHidden = true batteryFillView.layoutChain.width(0) batteryLab.text = "***" batteryLab.textColor = Self.batteryDefaultTextColor } private func applyBatteryDisplay(percent: Int?) { guard let percent else { applyBatteryUnknown() return } let p = min(max(percent, 0), 100) let isLow = p < 20 let color = isLow ? Self.batteryLowColor : Self.batteryNormalColor batteryIcon.image = UIImage(named: isLow ? "Home/member_battery_low" : "Home/member_battery") batteryFillView.isHidden = false batteryFillView.backgroundColor = color batteryFillView.layoutChain.width(Self.batteryFillMaxWidth * CGFloat(p) / 100.0) batteryLab.text = "\(p)%" batteryLab.textColor = color } private static func percent(from value: String?) -> Int? { guard let value else { return nil } let digits = value.filter(\.isNumber) guard let percent = Int(digits) else { return nil } return min(max(percent, 0), 100) } private static func phoneReportPreview(from usage: PhoneUsageTodayModel?) -> MemberPhoneReportPreview? { guard let usage else { return nil } let info = usage.phoneInfo return MemberPhoneReportPreview( screenTimeSeconds: screenTimeSeconds(from: info?.useTime), usageCount: usage.appCount, unlockCount: Int(info?.unlockCount.trimmed ?? "") ) } private static func screenTimeSeconds(from value: String?) -> Int? { guard let value else { return nil } let text = value.trimmed guard !text.isEmpty else { return nil } let hours = firstInteger(in: text, before: "小时") ?? 0 let minutes: Int if text.contains("分钟") { minutes = firstInteger(in: text, before: "分钟") ?? 0 } else { minutes = firstInteger(in: text, before: "分") ?? 0 } guard text.contains("小时") || text.contains("分") else { return nil } return max(0, hours * 3600 + minutes * 60) } private static func firstInteger(in text: String, before suffix: String) -> Int? { guard let range = text.range(of: suffix) else { return nil } let prefix = text[.. String { guard let minutes else { return "停留时长未知" } let value = max(0, minutes) let hours = value / 60 let remainingMinutes = value % 60 if hours > 0, remainingMinutes > 0 { return "停留\(hours)小时\(remainingMinutes)分" } if hours > 0 { return "停留\(hours)小时" } return "停留\(remainingMinutes)分钟" } private func setupUI() { addSubview(headerBgView) addSubview(navBarView) addSubview(scrollView) headerBgView.layoutChain .edges(excludingEdge: .bottom) .heightToWidth(160/375) navBarView.layoutChain .edges(excludingEdge: .bottom) .height(0) navBarHeightConstraint = navBarView.jh_constraint( .height, toAttribute: .notAnAttribute, otherView: nil, relation: .equal ) scrollView.layoutChain .topToBottomOfView(navBarView) .edges(excludingEdge: .top) scrollView.isScrollEnabled = false let lineView = UIView() lineView.backgroundColor = .black.withAlphaComponent(0.2) lineView.cornerRadius = 2 addSubview(lineView) lineView.layoutChain .top(9) .centerX() .width(40) .height(4) } lazy var headerBgView: UIImageView = { let view = UIImageView() view.image = UIImage(named: "Home/group_member_bg2") view.backgroundColor = .clear view.contentMode = .scaleAspectFill return view }() lazy var navBarView: UIView = { let view = UIView() view.backgroundColor = .clear view.clipsToBounds = true let icon = UIImageView(image: UIImage(named: "Home/member_logo")) view.addSubview(icon) icon.layoutChain .top(kStatusBarHeight + 12) .left(30) view.addSubview(inviteBtn) inviteBtn.layoutChain .centerY(icon) .right(15) .width(32) .heightToWidth(1) return view }() lazy var inviteBtn: UIButton = { let btn = UIButton(type: .custom) btn.setImage(UIImage(named: "Home/member_invite"), for: .normal) btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 20, bottom: 10, right: 15) return btn }() lazy var scrollView: UIScrollView = { let view = UIScrollView() view.backgroundColor = .clear view.showsVerticalScrollIndicator = false view.bounces = false view.addSubview(scrollContentView) scrollContentView.layoutChain.edges().widthToView(view) scrollContentView.addSubview(memberView) memberView.layoutChain .edges(excludingEdge: .bottom) scrollContentView.addSubview(interactionView) interactionView.isHidden = true interactionView.layoutChain .topToBottomOfView(memberView, offset: 12) .edgesHorzontal() scrollContentView.addSubview(todayTrackView) todayTrackView.layoutChain .topToBottomOfView(interactionView, offset: 15) todayTrackTopToInteraction = todayTrackView.jh_lastConstraint todayTrackTopToInteraction?.isActive = false todayTrackView.layoutChain .edgesHorzontal() todayTrackTopToMember = todayTrackView.jh_pinEdge(.top, toEdge: .bottom, of: memberView, offset: 12) scrollContentView.addSubview(phoneReportView) phoneReportView.layoutChain .topToBottomOfView(todayTrackView, offset: 15) .edgesHorzontal() .bottom(49 + kSafeBottomMargin) return view }() lazy var scrollContentView: UIView = { let view = UIView() view.backgroundColor = .clear return view }() // MARK: - 成员 lazy var memberView: UIView = { let view = UIView() view.backgroundColor = .clear let icon = UIImageView() icon.image = UIImage(named: "Home/group_name_icon") view.addSubview(icon) icon.layoutChain .top(23) .left(15) .width(18) .heightToWidth(1) view.addSubview(groupNameLab) groupNameLab.layoutChain .leftToRightOfView(icon, offset: 6) .centerY(icon) let groupBtnView = UIView() groupBtnView.backgroundColor = .white groupBtnView.cornerRadius = 8 view.addSubview(groupBtnView) groupBtnView.addSubview(changeGroupBtn) groupBtnView.layoutChain .centerY(icon) .right(15) .width(70) .height(24) changeGroupBtn.layoutChain .edges() let memberView = UIView() memberView.backgroundColor = .white memberView.cornerRadius = 20 memberView.addSubview(memberCV) view.addSubview(memberView) memberView.layoutChain .topToBottomOfView(icon, offset: 12) .edgesHorzontal(15) .height(100) memberCV.layoutChain .edges(all: 5, excludingEdge: .bottom) .bottom() view.addSubview(memberInfoView) memberInfoView.layoutChain .topToBottomOfView(memberView, offset: 12) .edgesHorzontal(15) .height(150) .bottom() return view }() lazy var groupNameLab: UILabel = { let label = UILabel() label.text = " " label.font = .systemFont(ofSize: 16, weight: .bold) label.textColor = UIColor(hexStr: "#353B4F") 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 btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 20, bottom: 10, right: 15) 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) return cv }() /// 成员信息 lazy var memberInfoView: UIView = { let view = UIView() view.backgroundColor = .white view.cornerRadius = 20 view.addSubview(memberNameLab) memberNameLab.layoutChain .top(20) .left(18) view.addSubview(tagView) tagView.layoutChain .leftToRightOfView(memberNameLab, offset: 6) .centerY(memberNameLab) .height(20) ownView.layoutChain.height(16) moodImageView.layoutChain.width(20).height(20) let statusView = UIView() statusView.backgroundColor = .clear statusView.addSubview(statusDotView) statusView.addSubview(statusLab) view.addSubview(statusView) statusView.layoutChain .right(18) .centerY(memberNameLab) statusDotView.layoutChain .left() .width(8) .heightToWidth(1) .centerY() statusLab.layoutChain .leftToRightOfView(statusDotView, offset: 4) .edgesVertical() .right() let locationIcon = UIImageView() locationIcon.image = UIImage(named: "Home/member_location") view.addSubview(locationIcon) locationIcon.layoutChain .topToBottomOfView(memberNameLab, offset: 17) .left(20) .width(16) .heightToWidth(1) view.addSubview(locationLab) locationLab.layoutChain .centerY(locationIcon) .leftToRightOfView(locationIcon, offset: 7) .compressionHorizontal(.defaultLow) view.addSubview(distanceView) distanceView.layoutChain .right(18) .centerY(locationLab) .compressionHorizontal(.required) locationLab.layoutChain.rightToLeftOfView(distanceView, offset: -30, relation: .lessThanOrEqual) view.addSubview(phoneModelView) view.addSubview(batteryView) view.addSubview(networkView) view.addSubview(weatherView) phoneModelView.layoutChain .topToBottomOfView(locationLab, offset: 20) .left() batteryView.layoutChain .topToView(phoneModelView) .leftToRightOfView(phoneModelView) .widthToView(phoneModelView) networkView.layoutChain .topToView(phoneModelView) .leftToRightOfView(batteryView) .widthToView(phoneModelView) weatherView.layoutChain .topToView(phoneModelView) .leftToRightOfView(networkView) .right() .widthToView(phoneModelView) return view }() lazy var memberNameLab: UILabel = { let label = UILabel() label.text = " " label.font = FontManager.youSheBiaoTiHei(14) label.textColor = UIColor(hexStr: "#293445") return label }() lazy var tagView: UIStackView = { let view = UIStackView(arrangedSubviews: [ownView, relationIconView, moodImageView]) view.axis = .horizontal view.alignment = .center view.spacing = 5 view.backgroundColor = .clear return view }() lazy var moodImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.isHidden = true return imageView }() lazy var relationIconView: RelationIconImageView = { RelationIconImageView() }() // 圈主 lazy var ownView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#E3F6FF") view.cornerRadius = 6 view.isHidden = true let label = UILabel() label.textColor = UIColor(hexStr: "#128CC6") label.font = .systemFont(ofSize: 10, weight: .bold) label.text = "圈主" view.addSubview(label) label.layoutChain .edgesVertical(2) .edgesHorzontal(8) return view }() /// 在线状态 lazy var statusDotView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#D8D8D8") view.cornerRadius = 4 return view }() lazy var statusLab: UILabel = { let label = UILabel() label.text = " " label.font = .systemFont(ofSize: 10, weight: .medium) label.textColor = UIColor(hexStr: "#353B4F") return label }() // 位置 lazy var locationLab: UILabel = { let label = UILabel() label.textColor = UIColor(hexStr: "#767676") label.font = .systemFont(ofSize: 12, weight: .medium) return label }() // 距离 lazy var distanceView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#353B4F") view.cornerRadius = 6 view.isHidden = true let label = UILabel() label.textColor = UIColor(hexStr: "#58EDFF") label.font = FontManager.youSheBiaoTiHei(10) label.text = "距离" view.addSubview(label) label.layoutChain .left(5) .edgesVertical(3) view.addSubview(distanceLab) distanceLab.layoutChain .leftToRightOfView(label, offset: 2) .right(5) .centerY() return view }() lazy var distanceLab: UILabel = { let label = UILabel() label.textColor = .white label.font = FontManager.youSheBiaoTiHei(10) return label }() /// 手机型号 lazy var phoneModelView: UIView = { let view = UIView() view.backgroundColor = .clear view.addSubview(phoneModelIcon) phoneModelIcon.layoutChain .top() .centerX() .width(24) .height(24) view.addSubview(phoneModelLab) phoneModelLab.layoutChain .topToBottomOfView(phoneModelIcon, offset: 5) .edgesHorzontal(5) .bottom() return view }() lazy var phoneModelIcon: UIImageView = { let view = UIImageView(image: UIImage(named: "Home/member_phone_unknown")) view.contentMode = .scaleAspectFit return view }() lazy var phoneModelLab: UILabel = { let label = UILabel() label.text = "***" label.textColor = UIColor(hexStr: "#353B4F") label.font = .systemFont(ofSize: 12, weight: .medium) label.textAlignment = .center label.numberOfLines = 0 return label }() /// 电量 lazy var batteryView: UIView = { let view = UIView() view.backgroundColor = .clear view.addSubview(batteryFillView) view.addSubview(batteryIcon) batteryIcon.layoutChain .top() .centerX() .width(24) .height(24) // 填充层叠在轮廓下方,内边距避开描边与右侧电极 batteryFillView.layoutChain .centerY(batteryIcon) .leftToView(batteryIcon, offset: 2) .height(12) .width(0) view.addSubview(batteryLab) batteryLab.layoutChain .topToBottomOfView(batteryIcon, offset: 5) .centerX() .bottom() return view }() lazy var batteryFillView: UIView = { let view = UIView() view.backgroundColor = Self.batteryNormalColor view.isHidden = true return view }() lazy var batteryIcon: UIImageView = { let view = UIImageView(image: UIImage(named: "Home/member_battery_unknown")) view.contentMode = .scaleAspectFit return view }() lazy var batteryLab: UILabel = { let label = UILabel() label.text = "***" label.textColor = UIColor(hexStr: "#353B4F") label.font = .systemFont(ofSize: 12, weight: .medium) return label }() /// 手机网络 lazy var networkView: UIView = { let view = UIView() view.backgroundColor = .clear view.addSubview(networkIcon) networkIcon.layoutChain .top() .centerX() .width(24) .height(24) view.addSubview(networkLab) networkLab.layoutChain .topToBottomOfView(networkIcon, offset: 5) .centerX() .bottom() return view }() lazy var networkIcon: UIImageView = { let view = UIImageView(image: UIImage(named: "Home/member_network_unknown")) view.contentMode = .scaleAspectFit return view }() lazy var networkLab: UILabel = { let label = UILabel() label.text = "***" label.textColor = UIColor(hexStr: "#353B4F") label.font = .systemFont(ofSize: 12, weight: .medium) return label }() /// 天气 lazy var weatherView: UIView = { let view = UIView() view.backgroundColor = .clear view.addSubview(weatherIcon) weatherIcon.layoutChain .top() .centerX() .width(24) .height(24) view.addSubview(weatherLab) weatherLab.layoutChain .topToBottomOfView(weatherIcon, offset: 5) .centerX() .bottom() return view }() lazy var weatherIcon: UIImageView = { let view = UIImageView(image: UIImage(named: "Home/member_weather_unknown")) view.contentMode = .scaleAspectFit return view }() lazy var weatherLab: UILabel = { let label = UILabel() label.text = "***" label.textColor = UIColor(hexStr: "#353B4F") label.font = .systemFont(ofSize: 12, weight: .medium) return label }() // MARK: - 手机报告 / APP使用记录 lazy var phoneReportView = MemberPhoneReportView() /// 保留 UI 实现,当前不挂载到 scroll 内容 lazy var appUsageView: MemberAppUsageView = { let view = MemberAppUsageView() view.isHidden = true return view }() // MARK: - 实时互动 lazy var interactionView: UIView = { let view = UIView() view.backgroundColor = .clear let icon = UIImageView() icon.image = UIImage(named: "Home/member_interaction") view.addSubview(icon) icon.layoutChain .top() .left(15) .width(18) .heightToWidth(1) let titleLab = UILabel() titleLab.text = "实时互动" titleLab.textColor = UIColor(hexStr: "#293445") titleLab.font = .systemFont(ofSize: 16, weight: .bold) view.addSubview(titleLab) titleLab.layoutChain .leftToRightOfView(icon, offset: 6) .centerY(icon) view.addSubview(emojiCard) view.addSubview(messageCard) emojiCard.layoutChain .topToBottomOfView(icon, offset: 12) .left(16) .height(58) .bottom() messageCard.layoutChain .topToView(emojiCard) .leftToRightOfView(emojiCard, offset: 10) .right(16) .widthToView(emojiCard) .bottomToView(emojiCard) return view }() lazy var emojiCard: UIView = { let view = UIView() view.backgroundColor = .white view.cornerRadius = 16 view.addSubview(emojiImageView) emojiImageView.layoutChain .top(5) .centerX() .width(30) .heightToWidth(1) let hintLab = UILabel() hintLab.text = "点击发送表情" hintLab.textColor = UIColor(hexStr: "#9AA1AE") hintLab.font = .systemFont(ofSize: 12, weight: .medium) hintLab.textAlignment = .center view.addSubview(hintLab) hintLab.layoutChain .topToBottomOfView(emojiImageView, offset: 6) .edgesHorzontal(8) return view }() lazy var messageCard: UIView = { let view = UIView() view.backgroundColor = .white view.cornerRadius = 16 let msgView = UIView() msgView.backgroundColor = UIColor(hexStr: "#F2F2F2") msgView.cornerRadius = 8 view.addSubview(msgView) msgView.addSubview(messageTextLab) msgView.layoutChain .top(9) .edgesHorzontal(10) .height(26) messageTextLab.layoutChain .edgesHorzontal(25) .centerY() let hintLab = UILabel() hintLab.text = "点击发送消息" hintLab.textColor = UIColor(hexStr: "#9AA1AE") hintLab.font = .systemFont(ofSize: 12, weight: .medium) hintLab.textAlignment = .center view.addSubview(hintLab) hintLab.layoutChain .topToBottomOfView(msgView, offset: 6) .edgesHorzontal(8) return view }() lazy var emojiImageView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFit return view }() lazy var messageTextLab: UILabel = { let label = UILabel() label.text = "暂时没有消息记录" label.textColor = UIColor(hexStr: "#293445") label.font = .systemFont(ofSize: 12, weight: .medium) label.textAlignment = .center return label }() // MARK: - 今日轨迹 lazy var todayTrackView: UIView = { let view = UIView() view.backgroundColor = .clear let icon = UIImageView() icon.image = UIImage(named: "Home/member_today_track") view.addSubview(icon) icon.layoutChain .top() .left(15) .width(18) .heightToWidth(1) let titleLab = UILabel() titleLab.text = "历史轨迹" titleLab.textColor = UIColor(hexStr: "#293445") titleLab.font = .systemFont(ofSize: 16, weight: .bold) view.addSubview(titleLab) titleLab.layoutChain .leftToRightOfView(icon, offset: 6) .centerY(icon) let infoView = UIView() infoView.backgroundColor = .white infoView.cornerRadius = 16 view.addSubview(infoView) infoView.layoutChain .topToBottomOfView(icon, offset: 12) .edgesHorzontal(15) .bottom() let mapIcon = UIImageView(image: UIImage(named: "Home/member_map")) infoView.addSubview(mapIcon) mapIcon.layoutChain .left(12) .edgesVertical(15) .width(80) .heightToWidth(1) let locationIcon = UIImageView(image: UIImage(named: "Home/member_location")) infoView.addSubview(locationIcon) locationIcon.layoutChain .topToView(mapIcon, offset: 11) .leftToRightOfView(mapIcon, offset: 14) .width(16) .heightToWidth(1) let stayTxtLab = UILabel() stayTxtLab.text = "今天停留了" stayTxtLab.textColor = UIColor(hexStr: "#293445") stayTxtLab.font = .systemFont(ofSize: 16, weight: .medium) view.addSubview(stayTxtLab) stayTxtLab.layoutChain .leftToRightOfView(locationIcon, offset: 6) .centerY(locationIcon) let stayNumView = UIView() stayNumView.backgroundColor = UIColor(hexStr: "#293445") stayNumView.cornerRadius = 6 infoView.addSubview(stayNumView) stayNumView.layoutChain .leftToRightOfView(stayTxtLab) .centerY(stayTxtLab) stayNumView.addSubview(stayNumLab) stayNumLab.layoutChain .edgesHorzontal(5) .edgesVertical() let stayTxtLab1 = UILabel() stayTxtLab1.text = "个地方" stayTxtLab1.textColor = UIColor(hexStr: "#293445") stayTxtLab1.font = .systemFont(ofSize: 16, weight: .medium) view.addSubview(stayTxtLab1) stayTxtLab1.layoutChain .leftToRightOfView(stayNumView, offset: 0) .centerY(stayTxtLab) view.addSubview(stayLocationView) stayLocationView.layoutChain .topToBottomOfView(locationIcon, offset: 12) .leftToRightOfView(mapIcon, offset: 14) .right() .bottomToView(mapIcon) view.addSubview(stayNodataView) stayNodataView.layoutChain .topToBottomOfView(stayTxtLab, offset: 14) .leftToView(stayTxtLab) return view }() lazy var stayNumLab: UILabel = { let label = UILabel() label.text = "0" label.textColor = UIColor(hexStr: "#58EDFF") label.font = FontManager.youSheBiaoTiHei(14) return label }() lazy var stayLocationView: UIView = { let view = UIView() view.backgroundColor = .clear view.isHidden = true let nowDotView = UIView() nowDotView.backgroundColor = UIColor(hexStr: "#3DC0FF") nowDotView.cornerRadius = 6 view.addSubview(nowDotView) nowDotView.layoutChain .top() .left() .width(12) .heightToWidth(1) let dotView = UIView() dotView.backgroundColor = .white dotView.cornerRadius = 3 nowDotView.addSubview(dotView) dotView.layoutChain .centerX() .centerY() .width(6) .heightToWidth(1) let dashLine = DashLineView() dashLine.backgroundColor = .clear view.addSubview(dashLine) dashLine.layoutChain .topToBottomOfView(nowDotView) .centerX(nowDotView) .width(1) .bottom() view.addSubview(stayLocationLab) stayLocationLab.layoutChain .leftToRightOfView(nowDotView, offset: 3) .centerY(nowDotView) .right(15) let stayStatusView = UIView() stayStatusView.backgroundColor = UIColor(hexStr: "#F2F2F2") stayStatusView.cornerRadius = 6 view.addSubview(stayStatusView) stayStatusView.layoutChain .topToBottomOfView(stayLocationLab, offset: 9) .leftToView(stayLocationLab) let clockIcon = UIImageView(image: UIImage(named: "Home/member_clock")) stayStatusView.addSubview(clockIcon) clockIcon.layoutChain .edges(all: 4, excludingEdge: .right) .width(12) .heightToWidth(1) stayStatusView.addSubview(stayStatusLab) stayStatusLab.layoutChain .leftToRightOfView(clockIcon, offset: 2) .right(7) .centerY() return view }() lazy var stayLocationLab: UILabel = { let label = UILabel() label.text = "当前 xxx 附近" label.textColor = UIColor(hexStr: "#293445") label.font = .systemFont(ofSize: 10, weight: .bold) return label }() lazy var stayStatusLab: UILabel = { let label = UILabel() label.text = "停留中..." // 停留多少时间 label.textColor = UIColor(hexStr: "#293445") label.font = .systemFont(ofSize: 8, weight: .bold) return label }() lazy var stayNodataView: UIView = { let view = UIView() view.backgroundColor = UIColor(hexStr: "#F2F2F2") view.cornerRadius = 6 view.isHidden = false let label = UILabel() label.text = "暂时没有停留记录" label.textColor = UIColor(hexStr: "#293445") label.font = .systemFont(ofSize: 12, weight: .medium) view.addSubview(label) label.layoutChain .edgesHorzontal(6) .edgesVertical(9) return view }() override init(frame: CGRect) { super.init(frame: frame) backgroundColor = UIColor(hexStr: "#FAFAFA") cornerRadius = 30 layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] _ = NetworkStatusMonitor.shared setupUI() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { NotificationCenter.default.removeObserver(self) } } /// 轻量网络状态监听,供面板读取当前路径 private final class NetworkStatusMonitor { static let shared = NetworkStatusMonitor() private let monitor = NWPathMonitor() private(set) var currentPath: NWPath private init() { currentPath = monitor.currentPath monitor.pathUpdateHandler = { [weak self] path in self?.currentPath = path } monitor.start(queue: DispatchQueue(label: "com.quicklocation.networkstatus")) } }