449 lines
12 KiB
Swift
449 lines
12 KiB
Swift
//
|
||
// GroupModel.swift
|
||
// QuickLocation
|
||
//
|
||
// Created by 八条 on 2026/5/30.
|
||
//
|
||
|
||
import ObjectMapper
|
||
import RxDataSources
|
||
import Kingfisher
|
||
|
||
private let kGroupTransformInt64 = TransformOf<Int64, Any>(fromJSON: { value in
|
||
switch value {
|
||
case let value as Int64:
|
||
return value
|
||
case let value as Int:
|
||
return Int64(value)
|
||
case let value as Double:
|
||
return Int64(value)
|
||
case let value as String:
|
||
return Int64(value)
|
||
default:
|
||
return nil
|
||
}
|
||
}, toJSON: { value in
|
||
value
|
||
})
|
||
|
||
/// 用户圈子
|
||
struct UserGroupResponse: BaseModelProtocol {
|
||
// 状态码
|
||
var code: String?
|
||
// 消息
|
||
var message: String?
|
||
//
|
||
var model: GroupModel?
|
||
|
||
init?(map: Map) {}
|
||
|
||
mutating func mapping(map: Map) {
|
||
code <- map["code"]
|
||
message <- map["msg"]
|
||
model <- map["data"]
|
||
}
|
||
}
|
||
|
||
/// 推荐圈子
|
||
struct RecommandGroupResponse: BaseModelProtocol {
|
||
// 状态码
|
||
var code: String?
|
||
// 消息
|
||
var message: String?
|
||
//
|
||
var list: [GroupInfoModel] = []
|
||
|
||
init?(map: Map) {}
|
||
|
||
mutating func mapping(map: Map) {
|
||
code <- map["code"]
|
||
message <- map["msg"]
|
||
list <- map["data"]
|
||
}
|
||
}
|
||
|
||
/// 圈子信息
|
||
struct GroupInfoResponse: BaseModelProtocol {
|
||
// 状态码
|
||
var code: String?
|
||
// 消息
|
||
var message: String?
|
||
// 圈子信息
|
||
var model: GroupInfoModel?
|
||
// 圈子成员列表
|
||
var list: [GroupMemberModel] = []
|
||
// 待审核数量
|
||
var reviewCount: Int = 0
|
||
|
||
init?(map: Map) {}
|
||
|
||
mutating func mapping(map: Map) {
|
||
code <- map["code"]
|
||
message <- map["msg"]
|
||
model <- map["data.attr"]
|
||
list <- map["data.employee"]
|
||
reviewCount <- map["data.reviewCount"]
|
||
}
|
||
}
|
||
|
||
/// 指定圈子的资料及成员列表
|
||
struct GroupUsersResponse: BaseModelProtocol {
|
||
var code: String?
|
||
var message: String?
|
||
var model: GroupInfoModel?
|
||
var list: [GroupMemberModel] = []
|
||
|
||
init?(map: Map) {}
|
||
|
||
mutating func mapping(map: Map) {
|
||
code <- (map["code"], kIntTransformStr)
|
||
message <- map["message"]
|
||
model <- map["data.group"]
|
||
list <- map["data.mates"]
|
||
}
|
||
|
||
func isValid(for groupKey: String) -> Bool {
|
||
code == "0" && model?.group_key == groupKey
|
||
}
|
||
}
|
||
|
||
/// 圈子类型模板列表
|
||
struct GroupTemplateResponse: BaseModelProtocol {
|
||
var code: String?
|
||
var message: String?
|
||
var list: [GroupTemplateModel] = []
|
||
|
||
init?(map: Map) {}
|
||
|
||
mutating func mapping(map: Map) {
|
||
code <- (map["code"], kIntTransformStr)
|
||
message <- map["message"]
|
||
list <- map["data"]
|
||
}
|
||
}
|
||
|
||
struct GroupTemplateModel: Mappable, Equatable {
|
||
var name: String = ""
|
||
var type: Int = 0
|
||
var limit: Int = -1
|
||
var icon: String = ""
|
||
|
||
init?(map: Map) {}
|
||
|
||
mutating func mapping(map: Map) {
|
||
name <- map["name"]
|
||
type <- (map["type"], kStrTransformInt)
|
||
limit <- (map["limit"], kStrTransformInt)
|
||
icon <- map["icon"]
|
||
}
|
||
}
|
||
|
||
struct ReviewMemberListResponse: BaseModelProtocol {
|
||
// 状态码
|
||
var code: String?
|
||
// 消息
|
||
var message: String?
|
||
/// 待审核列表
|
||
var list: [GroupMemberModel] = []
|
||
|
||
init?(map: Map) {}
|
||
|
||
mutating func mapping(map: Map) {
|
||
code <- map["code"]
|
||
message <- map["msg"]
|
||
list <- map["data"]
|
||
}
|
||
}
|
||
|
||
struct GroupModel: Mappable {
|
||
/// 当前选中的圈子
|
||
var default_group_key: String = ""
|
||
///
|
||
var has_unread_email: Bool = false
|
||
/// 圈子是否有人SOS
|
||
var is_anyone_sos: Bool = false
|
||
/// 圈子列表
|
||
var groups: [GroupInfoModel] = []
|
||
/// 当前选中圈子里的成员列表
|
||
var select_group_employee: [GroupMemberModel] = []
|
||
|
||
var score: Int = 0
|
||
|
||
init?(map: Map) {
|
||
|
||
}
|
||
|
||
mutating func mapping(map: Map) {
|
||
default_group_key <- map["default_group_key"]
|
||
has_unread_email <- map["has_unread_email"]
|
||
is_anyone_sos <- map["is_anyone_sos"]
|
||
score <- map["score"]
|
||
groups <- map["groups"]
|
||
select_group_employee <- map["select_group_employee"]
|
||
}
|
||
}
|
||
|
||
struct GroupInfoModel: Mappable, Equatable {
|
||
///
|
||
var group_key: String = ""
|
||
///
|
||
var is_owner: Bool = false
|
||
/// 圈子名字
|
||
var name: String = ""
|
||
/// 圈子icon
|
||
var icon_index: Int = 1
|
||
var groupIcon: UIImage {
|
||
UIImage(named: "GroupIcon/\(icon_index)") ?? UIImage()
|
||
}
|
||
/// 新版圈子类型及模板;icon_index 仅保留给旧数据兼容。
|
||
var group_type: Int = 0
|
||
var group_template: GroupTemplateModel?
|
||
var resolvedGroupType: Int {
|
||
if group_type > 0 { return group_type }
|
||
if let type = group_template?.type, type > 0 { return type }
|
||
return icon_index
|
||
}
|
||
var groupTemplateName: String {
|
||
let name = group_template?.name.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||
return name.isEmpty ? "未知类型" : name
|
||
}
|
||
var groupIconURL: String {
|
||
group_template?.icon.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||
}
|
||
/// 标签
|
||
var labels: [String] = []
|
||
/// 审核开关
|
||
var review: Bool = false
|
||
/// 人数
|
||
var people_no: Int = 0
|
||
/// 描述
|
||
var description: String = ""
|
||
/// 会员等级
|
||
var level: String = ""
|
||
/// 邀请码
|
||
var share_code: String = ""
|
||
|
||
init?(map: Map) {
|
||
|
||
}
|
||
|
||
mutating func mapping(map: Map) {
|
||
group_key <- map["group_key"]
|
||
is_owner <- map["is_owner"]
|
||
name <- map["name"]
|
||
icon_index <- map["icon_index"]
|
||
group_type <- (map["group_type"], kStrTransformInt)
|
||
group_template <- map["group_template"]
|
||
labels <- map["labels"]
|
||
level <- (map["level"], kIntTransformStr)
|
||
review <- map["review"]
|
||
description <- map["description"]
|
||
share_code <- map["share_code"]
|
||
people_no <- (map["people_no"], kStrTransformInt)
|
||
}
|
||
}
|
||
|
||
extension UIImageView {
|
||
@MainActor
|
||
func setGroupIcon(_ model: GroupInfoModel) {
|
||
setGroupIcon(url: model.groupIconURL, fallback: model.groupIcon)
|
||
}
|
||
|
||
@MainActor
|
||
func setGroupIcon(url: String, fallback: UIImage?) {
|
||
kf.cancelDownloadTask()
|
||
image = fallback
|
||
guard let imageURL = URL(string: url), !url.isEmpty else { return }
|
||
kf.setImage(with: imageURL, placeholder: fallback) { [weak self] result in
|
||
if case .failure = result {
|
||
self?.image = fallback
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
extension GroupInfoModel: IdentifiableType {
|
||
public typealias Identity = String
|
||
|
||
public var identity: String {
|
||
return group_key
|
||
}
|
||
}
|
||
|
||
struct CommonGroupModel: Mappable, Equatable {
|
||
var group_key: String = ""
|
||
var group_name: String = ""
|
||
|
||
init(groupKey: String, groupName: String) {
|
||
group_key = groupKey
|
||
group_name = groupName
|
||
}
|
||
|
||
init?(map: Map) {}
|
||
|
||
mutating func mapping(map: Map) {
|
||
group_key <- map["group_key"]
|
||
group_name <- map["group_name"]
|
||
}
|
||
|
||
static func deduplicated(_ groups: [CommonGroupModel]) -> [CommonGroupModel] {
|
||
var seenKeys = Set<String>()
|
||
var result: [CommonGroupModel] = []
|
||
for group in groups {
|
||
let key = group.group_key.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !key.isEmpty, seenKeys.insert(key).inserted else { continue }
|
||
result.append(group)
|
||
}
|
||
return result
|
||
}
|
||
}
|
||
|
||
struct GroupMemberModel: Mappable, Equatable {
|
||
///
|
||
var user_id: String = ""
|
||
/// 头像,对应本地图标
|
||
var head_pic: String = "1"
|
||
/// 用户头像
|
||
var userIcon: UIImage {
|
||
let iconName = head_pic.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !iconName.isEmpty,
|
||
let image = UIImage(named: "UserIcon/\(iconName)") else {
|
||
return UIImage(named: "UserIcon/1") ?? UIImage()
|
||
}
|
||
return image
|
||
}
|
||
/// 昵称
|
||
var nick_name: String = ""
|
||
/// 备注
|
||
var remark: String = ""
|
||
/// 会员等级 1普通 2白银 3永久 4钻石
|
||
var level: Int = 1
|
||
var vipIcon: UIImage {
|
||
switch level {
|
||
case 2: // 白银
|
||
return UIImage(named: "VipIcon/2") ?? UIImage()
|
||
case 3: // 永久
|
||
return UIImage(named: "VipIcon/1") ?? UIImage()
|
||
default:
|
||
return UIImage()
|
||
}
|
||
}
|
||
/// 状态:0、无状态 1、SOS 2、气泡等
|
||
var status: Int = 0
|
||
/// 入圈天数(若服务端下发)
|
||
var join_days: Int = 0
|
||
/// 入圈时间戳。新接口使用秒,历史数据可能使用毫秒。
|
||
var join_time: Int64 = 0
|
||
/// 是否在线
|
||
var is_online: Bool = false
|
||
/// 设备电量
|
||
var battery: String = ""
|
||
/// 心情:0 无,1-36 对应 MoodStatusCatalog
|
||
var mood: Int = 0
|
||
/// 最后在线时间
|
||
var last_active_time: Int64 = 0
|
||
/// 最后在线文本(MQTT 计算)
|
||
var lastUpdateText: String {
|
||
let nowMs = Date().timeIntervalSince1970
|
||
let msgTimeMs = Double(last_active_time)
|
||
let diffSec = nowMs - msgTimeMs
|
||
let isOnline = diffSec < 60
|
||
let lastUpdateText: String
|
||
if isOnline {
|
||
lastUpdateText = ""
|
||
} else if diffSec < 3600 {
|
||
lastUpdateText = "\(Int(diffSec / 60))分钟前"
|
||
} else if diffSec < 86400 {
|
||
lastUpdateText = "\(Int(diffSec / 3600))小时前"
|
||
} else {
|
||
lastUpdateText = "\(Int(diffSec / 86400))天前"
|
||
}
|
||
return lastUpdateText
|
||
}
|
||
/// 最后位置
|
||
var last_position: String = ""
|
||
/// 当前用户与该成员共同加入的圈子
|
||
var common_groups: [CommonGroupModel] = []
|
||
var lastLocation: String {
|
||
guard last_position.contains(":") else { return "" }
|
||
let arr = last_position.components(separatedBy: ":")
|
||
return arr.last ?? ""
|
||
}
|
||
|
||
init?(map: Map) {
|
||
|
||
}
|
||
|
||
mutating func mapping(map: Map) {
|
||
user_id <- map["user_id"]
|
||
head_pic <- map["head_pic"]
|
||
level <- (map["level"], kStrTransformInt)
|
||
nick_name <- map["nick_name"]
|
||
remark <- map["remark"]
|
||
battery <- map["battery"]
|
||
last_position <- map["last_position"]
|
||
is_online <- map["is_online"]
|
||
last_active_time <- map["last_active_time"]
|
||
status <- map["status"]
|
||
mood <- (map["mood"], kStrTransformInt)
|
||
join_days <- (map["join_days"], kStrTransformInt)
|
||
join_time <- (map["join_time"], kGroupTransformInt64)
|
||
common_groups <- map["common_groups"]
|
||
common_groups = CommonGroupModel.deduplicated(common_groups)
|
||
}
|
||
}
|
||
|
||
extension GroupMemberModel {
|
||
/// New responses use seconds; older payloads may still contain milliseconds.
|
||
var joinDate: Date? {
|
||
guard join_time > 0 else { return nil }
|
||
let seconds = join_time >= 100_000_000_000
|
||
? TimeInterval(join_time) / 1000
|
||
: TimeInterval(join_time)
|
||
return Date(timeIntervalSince1970: seconds)
|
||
}
|
||
}
|
||
|
||
enum GroupOwnerMatcher {
|
||
static func matches(userId: String, groupKey: String) -> Bool {
|
||
!userId.isEmpty && !groupKey.isEmpty && groupKey.contains(userId)
|
||
}
|
||
}
|
||
|
||
enum GroupMemberSorter {
|
||
/// 圈主 -> 当前用户 -> 在线成员 -> 其他成员,同优先级保持接口顺序。
|
||
static func sorted(_ members: [GroupMemberModel],
|
||
groupKey: String,
|
||
currentUserId: String) -> [GroupMemberModel] {
|
||
members.enumerated()
|
||
.sorted { lhs, rhs in
|
||
let lhsPriority = priority(lhs.element,
|
||
groupKey: groupKey,
|
||
currentUserId: currentUserId)
|
||
let rhsPriority = priority(rhs.element,
|
||
groupKey: groupKey,
|
||
currentUserId: currentUserId)
|
||
if lhsPriority != rhsPriority { return lhsPriority < rhsPriority }
|
||
return lhs.offset < rhs.offset
|
||
}
|
||
.map(\.element)
|
||
}
|
||
|
||
private static func priority(_ member: GroupMemberModel,
|
||
groupKey: String,
|
||
currentUserId: String) -> Int {
|
||
if GroupOwnerMatcher.matches(userId: member.user_id, groupKey: groupKey) { return 0 }
|
||
if !currentUserId.isEmpty, member.user_id == currentUserId { return 1 }
|
||
if member.is_online { return 2 }
|
||
return 3
|
||
}
|
||
}
|
||
|
||
extension GroupMemberModel: IdentifiableType {
|
||
public typealias Identity = String
|
||
|
||
public var identity: String {
|
||
return user_id
|
||
}
|
||
}
|