feat: add live module and optimize report list

This commit is contained in:
2026-07-08 14:25:14 +08:00
parent 8ca8bcf948
commit 4a78a0c21a
14 changed files with 3712 additions and 102 deletions

View File

@ -0,0 +1,242 @@
//
// LiveUIComponents.swift
// suixinkan
//
import Kingfisher
import PhotosUI
import SnapKit
import UIKit
/// UI Android 使
enum LiveUIFormatter {
///
static func timeRange(start: Int64, end: Int64) -> String {
guard start > 0 || end > 0 else { return "" }
let startText = timestampText(start)
let endText = timestampText(end)
if startText.isEmpty { return endText }
if endText.isEmpty { return startText }
return "\(startText) - \(endText)"
}
///
static func duration(_ seconds: Int64) -> String {
let value = max(seconds, 0)
let hour = value / 3600
let minute = (value % 3600) / 60
let second = value % 60
if hour > 0 {
return String(format: "%02lld:%02lld:%02lld", hour, minute, second)
}
return String(format: "%02lld:%02lld", minute, second)
}
///
static func date(_ date: Date?) -> String {
guard let date else { return "" }
return LiveAlbumListViewModel.dateString(date)
}
private static func timestampText(_ timestamp: Int64) -> String {
guard timestamp > 0 else { return "" }
let date = Date(timeIntervalSince1970: TimeInterval(timestamp))
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.locale = Locale(identifier: "zh_CN")
formatter.dateFormat = "yyyy-MM-dd HH:mm"
return formatter.string(from: date)
}
}
/// Android
final class LiveActionButton: UIButton {
enum Style {
case primary
case warning
case danger
case disabled
}
var buttonStyle: Style = .primary {
didSet { updateAppearance() }
}
override var isEnabled: Bool {
didSet { updateAppearance() }
}
///
init(title: String, image: UIImage? = nil) {
super.init(frame: .zero)
setTitle(title, for: .normal)
setImage(image, for: .normal)
titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
layer.cornerRadius = 8
clipsToBounds = true
tintColor = .white
imageEdgeInsets = UIEdgeInsets(top: 0, left: -4, bottom: 0, right: 4)
updateAppearance()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateAppearance() {
let effectiveStyle: Style = isEnabled ? buttonStyle : .disabled
switch effectiveStyle {
case .primary:
backgroundColor = UIColor(hex: 0x0066FF)
setTitleColor(.white, for: .normal)
tintColor = .white
case .warning:
backgroundColor = AppColor.warning
setTitleColor(.white, for: .normal)
tintColor = .white
case .danger:
backgroundColor = AppColor.danger
setTitleColor(.white, for: .normal)
tintColor = .white
case .disabled:
backgroundColor = AppColor.inputBackground
setTitleColor(UIColor(hex: 0xB6BECA), for: .normal)
tintColor = UIColor(hex: 0xB6BECA)
}
}
}
/// Kingfisher
final class LiveCoverImageView: UIImageView {
///
init(cornerRadius: CGFloat = 8) {
super.init(frame: .zero)
contentMode = .scaleAspectFill
clipsToBounds = true
backgroundColor = UIColor(hex: 0xF4F4F4)
layer.cornerRadius = cornerRadius
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
///
func setURL(_ text: String) {
if let url = URL(string: text), !text.isEmpty {
kf.setImage(with: url, placeholder: placeholderImage())
} else {
image = placeholderImage()
}
}
private func placeholderImage() -> UIImage? {
UIImage(named: "img_square_loading_big") ?? UIImage(systemName: "photo")
}
}
/// 线
final class LiveDashedUploadView: UIControl {
private let plusContainer = UIView()
private let plusImageView = UIImageView(image: UIImage(systemName: "plus"))
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private var dashLayer: CAShapeLayer?
/// 线
init(title: String = "点击上传图片", subtitle: String? = nil) {
super.init(frame: .zero)
setup(title: title, subtitle: subtitle)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
dashLayer?.removeFromSuperlayer()
let layer = CAShapeLayer()
layer.strokeColor = AppColor.border.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 2
layer.lineDashPattern = [6, 6]
layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 8).cgPath
self.layer.insertSublayer(layer, at: 0)
dashLayer = layer
}
private func setup(title: String, subtitle: String?) {
backgroundColor = .white
plusContainer.backgroundColor = AppColor.primary
plusContainer.layer.cornerRadius = 16
plusImageView.tintColor = .white
plusImageView.contentMode = .scaleAspectFit
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 14)
titleLabel.textColor = subtitle == nil ? UIColor(hex: 0xB6BECA) : UIColor(hex: 0x4B5563)
subtitleLabel.text = subtitle
subtitleLabel.font = .systemFont(ofSize: 12)
subtitleLabel.textColor = UIColor(hex: 0xB6BECA)
subtitleLabel.textAlignment = .center
addSubview(plusContainer)
plusContainer.addSubview(plusImageView)
addSubview(titleLabel)
addSubview(subtitleLabel)
plusContainer.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalToSuperview().offset(subtitle == nil ? -14 : -24)
make.size.equalTo(32)
}
plusImageView.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(18)
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(plusContainer.snp.bottom).offset(12)
make.centerX.equalToSuperview()
}
subtitleLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(8)
make.centerX.equalToSuperview()
}
}
}
///
enum LivePhotoPickerLoader {
/// PHPicker
static func loadImage(from result: PHPickerResult) async throws -> LivePickedMedia {
try await withCheckedThrowingContinuation { continuation in
let provider = result.itemProvider
guard provider.canLoadObject(ofClass: UIImage.self) else {
continuation.resume(throwing: OSSUploadError.unsupportedFileType)
return
}
provider.loadObject(ofClass: UIImage.self) { object, error in
if let error {
continuation.resume(throwing: error)
return
}
guard let image = object as? UIImage, let data = image.jpegData(compressionQuality: 0.9) else {
continuation.resume(throwing: OSSUploadError.emptyFile)
return
}
continuation.resume(
returning: LivePickedMedia(
data: data,
fileName: "image_\(UUID().uuidString).jpg",
size: Int64(data.count),
width: Int(image.size.width),
height: Int(image.size.height)
)
)
}
}
}
}