1186 lines
38 KiB
Swift
1186 lines
38 KiB
Swift
//
|
||
// 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[..<range.lowerBound]
|
||
let digits = prefix.reversed().prefix(while: \.isNumber).reversed()
|
||
return Int(String(digits))
|
||
}
|
||
|
||
private func applyStayPoints(_ stayPoints: [PhoneUsageStayPointModel]) {
|
||
stayNumLab.text = "\(stayPoints.count)"
|
||
guard let latest = stayPoints.max(by: {
|
||
($0.startDate ?? .distantPast) < ($1.startDate ?? .distantPast)
|
||
}) else {
|
||
stayLocationView.isHidden = true
|
||
stayNodataView.isHidden = false
|
||
stayLocationLab.text = ""
|
||
stayStatusLab.text = ""
|
||
return
|
||
}
|
||
|
||
let street = latest.address?.street.trimmed ?? ""
|
||
let formattedAddress = latest.address?.formattedAddress.trimmed ?? ""
|
||
stayLocationLab.text = !street.isEmpty ? street : (!formattedAddress.isEmpty ? formattedAddress : "未知地点")
|
||
stayStatusLab.text = Self.stayDurationText(minutes: latest.resolvedDurationMinutes)
|
||
stayLocationView.isHidden = false
|
||
stayNodataView.isHidden = true
|
||
}
|
||
|
||
private static func stayDurationText(minutes: Int?) -> 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"))
|
||
}
|
||
}
|