498 lines
18 KiB
Swift
498 lines
18 KiB
Swift
//
|
|
// PhoneReportDetailView.swift
|
|
// QuickLocation
|
|
//
|
|
|
|
import UIKit
|
|
|
|
final class PhoneReportDetailView: UIView {
|
|
|
|
lazy var navView: BaseNavigationView = {
|
|
let nav = BaseNavigationView(title: "手机报告")
|
|
return nav
|
|
}()
|
|
|
|
lazy var navBgView: UIImageView = {
|
|
let iv = UIImageView()
|
|
iv.image = UIImage(named: "Common/navBar_bg_2")
|
|
iv.contentMode = .scaleAspectFill
|
|
return iv
|
|
}()
|
|
|
|
let switchGroupBtn: UIButton = {
|
|
let btn = UIButton(type: .custom)
|
|
btn.backgroundColor = .white.withAlphaComponent(0.8)
|
|
btn.setTitle(" 切换圈子 ", for: .normal)
|
|
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
|
btn.setTitleColor(UIColor(hexStr: "#293445"), for: .normal)
|
|
btn.cornerRadius = 8
|
|
btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 20, bottom: 15, right: 18)
|
|
return btn
|
|
}()
|
|
|
|
lazy var memberCV: UICollectionView = {
|
|
let layout = UICollectionViewFlowLayout()
|
|
layout.scrollDirection = .horizontal
|
|
layout.itemSize = CGSize(width: 61, height: 90)
|
|
layout.minimumLineSpacing = 12
|
|
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
|
cv.backgroundColor = .white
|
|
cv.showsHorizontalScrollIndicator = false
|
|
cv.contentInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
|
|
cv.register(GroupMemberListCell.self)
|
|
cv.applyPhoneReportCardStyle(cornerRadius: 20)
|
|
return cv
|
|
}()
|
|
|
|
let scrollView = UIScrollView()
|
|
let contentView = UIView()
|
|
|
|
// MARK: Status
|
|
let statusTitleLab = UILabel()
|
|
let statusCard = UIView()
|
|
let batteryRing = PhoneBatteryRingView()
|
|
let estimateLab = UILabel()
|
|
let brightnessCard = PhoneStatusMiniCard(iconName: "Home/phone_brightness", title: "亮度")
|
|
let volumeCard = PhoneStatusMiniCard(iconName: "Home/phone_volume", title: "音量")
|
|
let wifiCard = PhoneStatusMiniCard(iconName: "Home/phone_wifi", title: "WIFI")
|
|
let deviceCard = UIView()
|
|
let deviceNameLab = UILabel()
|
|
let storageLab = UILabel()
|
|
let storageBar = UIProgressView(progressViewStyle: .default)
|
|
let deviceImage = UIImageView(image: UIImage(named: "Home/phone_device"))
|
|
|
|
// MARK: Charts
|
|
let screenSection = PhoneReportChartSectionView(
|
|
title: "屏幕使用时间",
|
|
summaryPrefix: "今日使用时长",
|
|
barColor: UIColor(hexStr: "#5EC8FF"),
|
|
highlightColor: UIColor(hexStr: "#2F9BFF")
|
|
)
|
|
let unlockSection = PhoneReportChartSectionView(
|
|
title: "手机使用次数",
|
|
summaryPrefix: "今日使用次数",
|
|
barColor: UIColor(hexStr: "#B7E86A"),
|
|
highlightColor: UIColor(hexStr: "#83DE00"),
|
|
valueSuffix: "次"
|
|
)
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
|
setupUI()
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
private func setupUI() {
|
|
addSubview(navBgView)
|
|
addSubview(navView)
|
|
|
|
navBgView.layoutChain
|
|
.edges(excludingEdge: .bottom)
|
|
.heightToWidth(160/375)
|
|
|
|
navView.layoutChain.top().edgesHorzontal().height(kNaviHeight)
|
|
navView.addRightButton(switchGroupBtn)
|
|
|
|
addSubview(memberCV)
|
|
memberCV.layoutChain
|
|
.topToBottomOfView(navView, offset: 8)
|
|
.edgesHorzontal(15)
|
|
.height(100)
|
|
|
|
addSubview(scrollView)
|
|
scrollView.showsVerticalScrollIndicator = false
|
|
scrollView.layoutChain
|
|
.topToBottomOfView(memberCV, offset: 12)
|
|
.edgesHorzontal()
|
|
.bottom()
|
|
scrollView.addSubview(contentView)
|
|
contentView.layoutChain.edges().widthToView(scrollView)
|
|
|
|
statusTitleLab.text = "手机状态"
|
|
statusTitleLab.font = .systemFont(ofSize: 16, weight: .bold)
|
|
statusTitleLab.textColor = UIColor(hexStr: "#293445")
|
|
contentView.addSubview(statusTitleLab)
|
|
statusTitleLab.layoutChain.top().left(15)
|
|
|
|
statusCard.backgroundColor = .white
|
|
statusCard.applyPhoneReportCardStyle(cornerRadius: 20)
|
|
contentView.addSubview(statusCard)
|
|
statusCard.layoutChain
|
|
.topToBottomOfView(statusTitleLab, offset: 10)
|
|
.edgesHorzontal(15)
|
|
.height(175)
|
|
|
|
let leftPanel = UIView()
|
|
leftPanel.backgroundColor = .clear
|
|
leftPanel.cornerRadius = 14
|
|
statusCard.addSubview(leftPanel)
|
|
leftPanel.layoutChain
|
|
.edges(excludingEdge: .right)
|
|
.width(136)
|
|
|
|
leftPanel.addSubview(batteryRing)
|
|
batteryRing.layoutChain.top(23).centerX().width(100).height(100)
|
|
|
|
estimateLab.font = .systemFont(ofSize: 12, weight: .medium)
|
|
estimateLab.textColor = UIColor(hexStr: "#6B7280")
|
|
estimateLab.textAlignment = .center
|
|
estimateLab.backgroundColor = UIColor(hexStr: "#E8EEF5")
|
|
estimateLab.cornerRadius = 10
|
|
leftPanel.addSubview(estimateLab)
|
|
estimateLab.layoutChain
|
|
.topToBottomOfView(batteryRing, offset: 12)
|
|
.centerX()
|
|
.height(22)
|
|
.width(120)
|
|
|
|
let rightStack = UIStackView(arrangedSubviews: [brightnessCard, volumeCard, wifiCard])
|
|
rightStack.axis = .horizontal
|
|
rightStack.distribution = .fillEqually
|
|
rightStack.spacing = 8
|
|
statusCard.addSubview(rightStack)
|
|
rightStack.layoutChain
|
|
.top(12)
|
|
.leftToRightOfView(leftPanel, offset: 10)
|
|
.right(12)
|
|
.height(64)
|
|
|
|
deviceCard.backgroundColor = UIColor(hexStr: "#F7FBFF")
|
|
deviceCard.cornerRadius = 12
|
|
statusCard.addSubview(deviceCard)
|
|
deviceCard.layoutChain
|
|
.topToBottomOfView(rightStack, offset: 8)
|
|
.leftToView(rightStack)
|
|
.right(12)
|
|
.bottom(12)
|
|
|
|
deviceNameLab.font = .systemFont(ofSize: 13, weight: .bold)
|
|
deviceNameLab.textColor = UIColor(hexStr: "#293445")
|
|
deviceCard.addSubview(deviceNameLab)
|
|
deviceNameLab.layoutChain.top(10).left(10).right(60)
|
|
|
|
storageLab.font = .systemFont(ofSize: 11, weight: .medium)
|
|
storageLab.textColor = UIColor(hexStr: "#6B7280")
|
|
deviceCard.addSubview(storageLab)
|
|
storageLab.layoutChain.topToBottomOfView(deviceNameLab, offset: 4).left(10)
|
|
|
|
storageBar.progressTintColor = UIColor(hexStr: "#5EC8FF")
|
|
storageBar.trackTintColor = UIColor(hexStr: "#E5E7EB")
|
|
deviceCard.addSubview(storageBar)
|
|
storageBar.layoutChain
|
|
.topToBottomOfView(storageLab, offset: 6)
|
|
.left(10)
|
|
.right(60)
|
|
.height(4)
|
|
.bottom(10)
|
|
|
|
deviceImage.contentMode = .scaleAspectFit
|
|
deviceCard.addSubview(deviceImage)
|
|
deviceImage.layoutChain.right(8).centerY().width(36).height(54)
|
|
|
|
contentView.addSubview(screenSection)
|
|
screenSection.layoutChain
|
|
.topToBottomOfView(statusCard, offset: 16)
|
|
.edgesHorzontal(15)
|
|
|
|
contentView.addSubview(unlockSection)
|
|
unlockSection.layoutChain
|
|
.topToBottomOfView(screenSection, offset: 16)
|
|
.edgesHorzontal(15)
|
|
.bottom(24)
|
|
}
|
|
|
|
func apply(_ snap: PhoneReportDetailViewModel.StatusSnapshot) {
|
|
batteryRing.configure(percent: snap.batteryPercent, text: snap.batteryText)
|
|
estimateLab.text = "预计使用\(snap.estimateText)"
|
|
brightnessCard.setValue(snap.brightnessText)
|
|
volumeCard.setValue(snap.volumeText)
|
|
wifiCard.setValue(snap.wifiText)
|
|
deviceNameLab.text = snap.deviceText
|
|
storageLab.text = snap.storageText
|
|
storageBar.progress = Float(snap.storageRatio)
|
|
screenSection.setSummary("\(screenSection.summaryPrefix) \(snap.screenTodayText)")
|
|
unlockSection.setSummary("\(unlockSection.summaryPrefix) \(snap.unlockTodayText)次")
|
|
screenSection.setBars(snap.screenBars)
|
|
unlockSection.setBars(snap.unlockBars)
|
|
}
|
|
}
|
|
|
|
private extension UIView {
|
|
func applyPhoneReportCardStyle(cornerRadius: CGFloat) {
|
|
layer.cornerRadius = cornerRadius
|
|
clipsToBounds = false
|
|
layer.masksToBounds = false
|
|
layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.1).cgColor
|
|
layer.shadowOffset = CGSize(width: 0, height: 2)
|
|
layer.shadowOpacity = 1
|
|
layer.shadowRadius = 10
|
|
}
|
|
}
|
|
|
|
// MARK: - Subviews
|
|
|
|
final class PhoneBatteryRingView: UIView {
|
|
private let track = CAShapeLayer()
|
|
private let progress = CAShapeLayer()
|
|
private let valueLab = UILabel()
|
|
private let bolt = UIImageView(image: UIImage(systemName: "bolt.fill"))
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
track.fillColor = UIColor.clear.cgColor
|
|
track.strokeColor = UIColor(hexStr: "#DEFFD8").cgColor
|
|
track.lineWidth = 11
|
|
layer.addSublayer(track)
|
|
progress.fillColor = UIColor.clear.cgColor
|
|
progress.strokeColor = UIColor(hexStr: "#27DB4B").cgColor
|
|
progress.lineWidth = 11
|
|
progress.lineCap = .round
|
|
progress.strokeEnd = 0
|
|
layer.addSublayer(progress)
|
|
|
|
bolt.tintColor = UIColor(hexStr: "#27DB4B")
|
|
bolt.contentMode = .scaleAspectFit
|
|
addSubview(bolt)
|
|
valueLab.font = .systemFont(ofSize: 16, weight: .heavy)
|
|
valueLab.textColor = UIColor(hexStr: "#293445")
|
|
valueLab.textAlignment = .center
|
|
addSubview(valueLab)
|
|
valueLab.layoutChain.centerX().centerY()
|
|
bolt.layoutChain.centerY(valueLab).rightToLeftOfView(valueLab)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
let radius = min(bounds.width, bounds.height) / 2 - 6
|
|
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: -.pi / 2, endAngle: 1.5 * .pi, clockwise: true)
|
|
track.path = path.cgPath
|
|
progress.path = path.cgPath
|
|
}
|
|
|
|
func configure(percent: Int?, text: String) {
|
|
valueLab.text = text
|
|
progress.strokeEnd = CGFloat(max(0, min(100, percent ?? 0))) / 100
|
|
}
|
|
}
|
|
|
|
final class PhoneStatusMiniCard: UIView {
|
|
private let icon = UIImageView()
|
|
private let titleLab = UILabel()
|
|
private let valueLab = UILabel()
|
|
|
|
init(iconName: String, title: String) {
|
|
super.init(frame: .zero)
|
|
backgroundColor = UIColor(hexStr: "#F7FBFF")
|
|
cornerRadius = 12
|
|
icon.image = UIImage(named: iconName)
|
|
icon.contentMode = .scaleAspectFit
|
|
addSubview(icon)
|
|
icon.layoutChain.top(7).left(5).width(16).height(16)
|
|
titleLab.text = title
|
|
titleLab.font = .systemFont(ofSize: 10, weight: .medium)
|
|
titleLab.textColor = UIColor(hexStr: "#AAAAAA")
|
|
titleLab.textAlignment = .center
|
|
addSubview(titleLab)
|
|
titleLab.layoutChain.leftToRightOfView(icon, offset: 4).right(5)
|
|
valueLab.font = .systemFont(ofSize: 16, weight: .medium)
|
|
valueLab.textColor = UIColor(hexStr: "#293445")
|
|
valueLab.textAlignment = .center
|
|
addSubview(valueLab)
|
|
valueLab.layoutChain.topToBottomOfView(icon, offset: 19).edgesHorzontal(2).bottom(16)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
func setValue(_ text: String) {
|
|
valueLab.text = text
|
|
}
|
|
}
|
|
|
|
final class PhoneReportChartSectionView: UIView {
|
|
let summaryPrefix: String
|
|
private let titleLab = UILabel()
|
|
private let summaryLab = UILabel()
|
|
private let dayBtn = UIButton(type: .system)
|
|
private let weekBtn = UIButton(type: .system)
|
|
private let chart = PhoneReportBarChartView()
|
|
private let barColor: UIColor
|
|
private let highlightColor: UIColor
|
|
private let valueSuffix: String
|
|
|
|
var onModeChanged: ((PhoneReportDetailViewModel.ChartMode) -> Void)?
|
|
|
|
init(title: String, summaryPrefix: String, barColor: UIColor, highlightColor: UIColor, valueSuffix: String = "") {
|
|
self.summaryPrefix = summaryPrefix
|
|
self.barColor = barColor
|
|
self.highlightColor = highlightColor
|
|
self.valueSuffix = valueSuffix
|
|
super.init(frame: .zero)
|
|
titleLab.text = title
|
|
titleLab.font = .systemFont(ofSize: 16, weight: .bold)
|
|
titleLab.textColor = UIColor(hexStr: "#293445")
|
|
addSubview(titleLab)
|
|
titleLab.layoutChain.top().left()
|
|
|
|
let card = UIView()
|
|
card.backgroundColor = .white
|
|
card.applyPhoneReportCardStyle(cornerRadius: 20)
|
|
addSubview(card)
|
|
card.layoutChain.topToBottomOfView(titleLab, offset: 10).edgesHorzontal().bottom().height(180)
|
|
|
|
summaryLab.font = .systemFont(ofSize: 13, weight: .medium)
|
|
summaryLab.textColor = UIColor(hexStr: "#293445")
|
|
card.addSubview(summaryLab)
|
|
summaryLab.layoutChain.top(14).left(14)
|
|
|
|
configureToggle(dayBtn, title: "天", selected: true)
|
|
configureToggle(weekBtn, title: "周", selected: false)
|
|
let toggle = UIStackView(arrangedSubviews: [weekBtn, dayBtn])
|
|
toggle.axis = .horizontal
|
|
toggle.spacing = 0
|
|
toggle.backgroundColor = UIColor(hexStr: "#F3F4F6")
|
|
toggle.cornerRadius = 12
|
|
card.addSubview(toggle)
|
|
toggle.layoutChain.centerY(summaryLab).right(14).height(28)
|
|
|
|
dayBtn.addTarget(self, action: #selector(tapDay), for: .touchUpInside)
|
|
weekBtn.addTarget(self, action: #selector(tapWeek), for: .touchUpInside)
|
|
|
|
card.addSubview(chart)
|
|
chart.layoutChain.topToBottomOfView(summaryLab, offset: 16).edgesHorzontal(14).bottom(14)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
private func configureToggle(_ btn: UIButton, title: String, selected: Bool) {
|
|
btn.setTitle(title, for: .normal)
|
|
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .bold)
|
|
btn.contentEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
|
|
applyToggle(btn, selected: selected)
|
|
}
|
|
|
|
private func applyToggle(_ btn: UIButton, selected: Bool) {
|
|
btn.backgroundColor = selected ? .white : .clear
|
|
btn.setTitleColor(selected ? UIColor(hexStr: "#293445") : UIColor(hexStr: "#9CA3AF"), for: .normal)
|
|
btn.cornerRadius = 12
|
|
}
|
|
|
|
@objc private func tapDay() {
|
|
applyToggle(dayBtn, selected: true)
|
|
applyToggle(weekBtn, selected: false)
|
|
onModeChanged?(.day)
|
|
}
|
|
|
|
@objc private func tapWeek() {
|
|
applyToggle(dayBtn, selected: false)
|
|
applyToggle(weekBtn, selected: true)
|
|
onModeChanged?(.week)
|
|
}
|
|
|
|
func setSummary(_ text: String) {
|
|
summaryLab.text = text
|
|
}
|
|
|
|
func setBars(_ bars: [PhoneReportDetailViewModel.ChartBar]) {
|
|
chart.configure(bars: bars, color: barColor, highlight: highlightColor, suffix: valueSuffix)
|
|
}
|
|
}
|
|
|
|
final class PhoneReportBarChartView: UIView {
|
|
private var bars: [PhoneReportDetailViewModel.ChartBar] = []
|
|
private var color: UIColor = .systemBlue
|
|
private var highlight: UIColor = .systemBlue
|
|
private var suffix = ""
|
|
private var barLayers: [CALayer] = []
|
|
private var labelViews: [UILabel] = []
|
|
private var tipLab = UILabel()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
tipLab.font = .systemFont(ofSize: 11, weight: .bold)
|
|
tipLab.textColor = .white
|
|
tipLab.backgroundColor = UIColor(hexStr: "#2F9BFF")
|
|
tipLab.textAlignment = .center
|
|
tipLab.cornerRadius = 8
|
|
tipLab.clipsToBounds = true
|
|
tipLab.isHidden = true
|
|
addSubview(tipLab)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
func configure(bars: [PhoneReportDetailViewModel.ChartBar], color: UIColor, highlight: UIColor, suffix: String) {
|
|
self.bars = bars
|
|
self.color = color
|
|
self.highlight = highlight
|
|
self.suffix = suffix
|
|
setNeedsLayout()
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
barLayers.forEach { $0.removeFromSuperlayer() }
|
|
barLayers.removeAll()
|
|
labelViews.forEach { $0.removeFromSuperview() }
|
|
labelViews.removeAll()
|
|
tipLab.isHidden = true
|
|
guard !bars.isEmpty else { return }
|
|
|
|
let maxVal = max(bars.map(\.value).max() ?? 1, 1)
|
|
let count = bars.count
|
|
let labelH: CGFloat = 16
|
|
let chartH = bounds.height - labelH - 24
|
|
let spacing: CGFloat = 8
|
|
let barW = max(8, (bounds.width - spacing * CGFloat(count - 1)) / CGFloat(count))
|
|
|
|
for (i, bar) in bars.enumerated() {
|
|
let ratio = CGFloat(bar.value) / CGFloat(maxVal)
|
|
let h = max(4, chartH * max(0.05, ratio))
|
|
let x = CGFloat(i) * (barW + spacing)
|
|
let y = bounds.height - labelH - h
|
|
let layer = CALayer()
|
|
layer.frame = CGRect(x: x, y: y, width: barW, height: h)
|
|
layer.backgroundColor = (bar.isToday ? highlight : color.withAlphaComponent(0.35)).cgColor
|
|
layer.cornerRadius = 4
|
|
self.layer.addSublayer(layer)
|
|
barLayers.append(layer)
|
|
|
|
let lab = UILabel()
|
|
lab.text = bar.label
|
|
lab.font = .systemFont(ofSize: 10, weight: .medium)
|
|
lab.textColor = UIColor(hexStr: "#9CA3AF")
|
|
lab.textAlignment = .center
|
|
lab.frame = CGRect(x: x - 6, y: bounds.height - labelH, width: barW + 12, height: labelH)
|
|
addSubview(lab)
|
|
labelViews.append(lab)
|
|
|
|
if bar.isToday, bar.value > 0 {
|
|
let hours = bar.value / 3600
|
|
let mins = (bar.value % 3600) / 60
|
|
let text: String
|
|
if suffix == "次" {
|
|
text = "\(bar.value)次"
|
|
} else if hours > 0 {
|
|
text = mins > 0 ? "\(hours)h \(mins)m" : "\(hours)h"
|
|
} else {
|
|
text = "\(mins)m"
|
|
}
|
|
tipLab.text = " \(text) "
|
|
tipLab.sizeToFit()
|
|
tipLab.frame = CGRect(
|
|
x: max(0, x + barW / 2 - tipLab.bounds.width / 2 - 6),
|
|
y: max(0, y - 22),
|
|
width: tipLab.bounds.width + 12,
|
|
height: 18
|
|
)
|
|
tipLab.backgroundColor = highlight
|
|
tipLab.isHidden = false
|
|
}
|
|
}
|
|
}
|
|
}
|