- 通用弹窗

- 解散圈子弹窗
- 退出登录、重新登录 config接口、IM登录  调用调整
- 个人中心 接口接入
- 账号与安全
This commit is contained in:
linshujie 2026-06-10 18:53:35 +08:00
parent 5381d1ffda
commit 5cb6414373
46 changed files with 1892 additions and 140 deletions

View File

@ -196,6 +196,13 @@
30EFF3A82FD7C6A400EB35D4 /* GroupSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3A72FD7C6A400EB35D4 /* GroupSettingViewModel.swift */; };
30EFF3AE2FD7FF1400EB35D4 /* TextInputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3AC2FD7FF1400EB35D4 /* TextInputViewController.swift */; };
30EFF3B02FD8122E00EB35D4 /* GroupTagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3AF2FD8122E00EB35D4 /* GroupTagListView.swift */; };
30EFF3B32FD8F1C200EB35D4 /* ReviewMemberListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3B22FD8F1C200EB35D4 /* ReviewMemberListView.swift */; };
30EFF3B52FD8F1D000EB35D4 /* ReviewMemberListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3B42FD8F1D000EB35D4 /* ReviewMemberListVC.swift */; };
30EFF3B72FD8F86200EB35D4 /* ReviewMemberListVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3B62FD8F86200EB35D4 /* ReviewMemberListVM.swift */; };
30EFF3B92FD8FC5200EB35D4 /* VerificationPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */; };
30EFF3BB2FD90D7600EB35D4 /* ConfirmPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3BA2FD90D7600EB35D4 /* ConfirmPopVC.swift */; };
30EFF3BE2FD958A100EB35D4 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3BD2FD958A100EB35D4 /* AccountView.swift */; };
30EFF3C02FD958AE00EB35D4 /* AccountVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EFF3BF2FD958AE00EB35D4 /* AccountVC.swift */; };
C49B37352A45A02C28FF41BA /* Pods_QuickLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1C77B42994F352054070537 /* Pods_QuickLocation.framework */; };
/* End PBXBuildFile section */
@ -399,6 +406,13 @@
30EFF3A72FD7C6A400EB35D4 /* GroupSettingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSettingViewModel.swift; sourceTree = "<group>"; };
30EFF3AC2FD7FF1400EB35D4 /* TextInputViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputViewController.swift; sourceTree = "<group>"; };
30EFF3AF2FD8122E00EB35D4 /* GroupTagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupTagListView.swift; sourceTree = "<group>"; };
30EFF3B22FD8F1C200EB35D4 /* ReviewMemberListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewMemberListView.swift; sourceTree = "<group>"; };
30EFF3B42FD8F1D000EB35D4 /* ReviewMemberListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewMemberListVC.swift; sourceTree = "<group>"; };
30EFF3B62FD8F86200EB35D4 /* ReviewMemberListVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewMemberListVM.swift; sourceTree = "<group>"; };
30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationPopView.swift; sourceTree = "<group>"; };
30EFF3BA2FD90D7600EB35D4 /* ConfirmPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmPopVC.swift; sourceTree = "<group>"; };
30EFF3BD2FD958A100EB35D4 /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = "<group>"; };
30EFF3BF2FD958AE00EB35D4 /* AccountVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountVC.swift; sourceTree = "<group>"; };
3E4359082FC48D26003470A5 /* QuickLocation.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickLocation.app; sourceTree = BUILT_PRODUCTS_DIR; };
93647DF3683AA5E71EC2FB1A /* Pods-QuickLocation.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.release.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.release.xcconfig"; sourceTree = "<group>"; };
D1C77B42994F352054070537 /* Pods_QuickLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuickLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -768,8 +782,10 @@
305A76242FCA8C7000227D26 /* GroupViewController.swift */,
305A76252FCA8C7000227D26 /* GroupViewModel.swift */,
305A76232FCA8C7000227D26 /* GroupView.swift */,
30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */,
307073E42FD18A20004C37CC /* GroupChat */,
30EFF3A22FD7C58400EB35D4 /* GroupSetting */,
30EFF3B12FD8F19E00EB35D4 /* ReviewMemberList */,
3062E8B82FCEAC5600CEF511 /* CreateGroup */,
30BAB8612FCD714700C33B5C /* Join */,
30BAB84B2FCD2FA400C33B5C /* InviteJoin */,
@ -821,9 +837,10 @@
305A76392FCA8C7000227D26 /* Mine */ = {
isa = PBXGroup;
children = (
305A76362FCA8C7000227D26 /* MineView.swift */,
305A76372FCA8C7000227D26 /* MineViewController.swift */,
305A76382FCA8C7000227D26 /* MineViewModel.swift */,
305A76362FCA8C7000227D26 /* MineView.swift */,
30EFF3BC2FD9585200EB35D4 /* Account */,
);
path = Mine;
sourceTree = "<group>";
@ -947,6 +964,7 @@
305A76692FCA8C7000227D26 /* Pop */ = {
isa = PBXGroup;
children = (
30EFF3BA2FD90D7600EB35D4 /* ConfirmPopVC.swift */,
305A76642FCA8C7000227D26 /* DLAlertPopVC.swift */,
305A76652FCA8C7000227D26 /* DLCustomPopVC.swift */,
305A76662FCA8C7000227D26 /* DLSheetPopVC.swift */,
@ -1110,6 +1128,25 @@
path = TextInput;
sourceTree = "<group>";
};
30EFF3B12FD8F19E00EB35D4 /* ReviewMemberList */ = {
isa = PBXGroup;
children = (
30EFF3B42FD8F1D000EB35D4 /* ReviewMemberListVC.swift */,
30EFF3B22FD8F1C200EB35D4 /* ReviewMemberListView.swift */,
30EFF3B62FD8F86200EB35D4 /* ReviewMemberListVM.swift */,
);
path = ReviewMemberList;
sourceTree = "<group>";
};
30EFF3BC2FD9585200EB35D4 /* Account */ = {
isa = PBXGroup;
children = (
30EFF3BF2FD958AE00EB35D4 /* AccountVC.swift */,
30EFF3BD2FD958A100EB35D4 /* AccountView.swift */,
);
path = Account;
sourceTree = "<group>";
};
3E4358FF2FC48D26003470A5 = {
isa = PBXGroup;
children = (
@ -1348,11 +1385,13 @@
305A76B72FCA8C7000227D26 /* UIView+Extension.swift in Sources */,
305A76B82FCA8C7000227D26 /* UIViewController+Extension.swift in Sources */,
305A76B92FCA8C7000227D26 /* URL+Extension.swift in Sources */,
30EFF3BE2FD958A100EB35D4 /* AccountView.swift in Sources */,
305A76BA2FCA8C7000227D26 /* Wrapper.swift in Sources */,
305A76BB2FCA8C7000227D26 /* BaseModelNew.swift in Sources */,
305A76BC2FCA8C7000227D26 /* ListModel.swift in Sources */,
305A76BD2FCA8C7000227D26 /* PaginationModel.swift in Sources */,
305A76BE2FCA8C7000227D26 /* ResponseModel.swift in Sources */,
30EFF3C02FD958AE00EB35D4 /* AccountVC.swift in Sources */,
305A76BF2FCA8C7000227D26 /* ListService.swift in Sources */,
305A76C02FCA8C7000227D26 /* BaseNavigationController.swift in Sources */,
305A76C12FCA8C7000227D26 /* BaseViewController.swift in Sources */,
@ -1396,8 +1435,10 @@
305A76DF2FCA8C7000227D26 /* Single+ObjectMapper.swift in Sources */,
30DC18602FD12A020041DCD1 /* VipWaivePopView.swift in Sources */,
305A76E02FCA8C7000227D26 /* GroupView.swift in Sources */,
30EFF3BB2FD90D7600EB35D4 /* ConfirmPopVC.swift in Sources */,
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */,
305A76E12FCA8C7000227D26 /* GroupViewController.swift in Sources */,
30EFF3B52FD8F1D000EB35D4 /* ReviewMemberListVC.swift in Sources */,
30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */,
305A76E22FCA8C7000227D26 /* GroupViewModel.swift in Sources */,
305A76E32FCA8C7000227D26 /* GroupMemberView.swift in Sources */,
@ -1408,6 +1449,7 @@
3062E8B52FCE6BBA00CEF511 /* ScanVC.swift in Sources */,
305A76E72FCA8C7000227D26 /* LoginView.swift in Sources */,
305A76E82FCA8C7000227D26 /* LoginViewController.swift in Sources */,
30EFF3B92FD8FC5200EB35D4 /* VerificationPopView.swift in Sources */,
305A76E92FCA8C7000227D26 /* LoginViewModel.swift in Sources */,
3062E8BC2FCEAC7100CEF511 /* CreateGroupVC.swift in Sources */,
30BAB8632FCD716C00C33B5C /* JoinGroupVC.swift in Sources */,
@ -1432,6 +1474,7 @@
305A76F72FCA8C7000227D26 /* RouterTarget.swift in Sources */,
307073E52FD18A20004C37CC /* GroupChatView.swift in Sources */,
307073E62FD18A20004C37CC /* GroupChatVC.swift in Sources */,
30EFF3B32FD8F1C200EB35D4 /* ReviewMemberListView.swift in Sources */,
305A76F82FCA8C7000227D26 /* DLAlert.swift in Sources */,
305A76F92FCA8C7000227D26 /* DLToast.swift in Sources */,
305A76FA2FCA8C7000227D26 /* DLEmptyDataSet.swift in Sources */,
@ -1461,6 +1504,7 @@
305A770F2FCA8C7000227D26 /* DLCustomPopVC.swift in Sources */,
30EFF29B2FD668C900EB35D4 /* VoiceRecordView.swift in Sources */,
305A77102FCA8C7000227D26 /* DLSheetPopVC.swift in Sources */,
30EFF3B72FD8F86200EB35D4 /* ReviewMemberListVM.swift in Sources */,
305A77112FCA8C7000227D26 /* DLViewTransition.m in Sources */,
3062E8BA2FCEAC6500CEF511 /* CreateGroupView.swift in Sources */,
305A77192FCA8C7000227D26 /* CollectionHFlowLayout.swift in Sources */,

View File

@ -114,6 +114,8 @@ public extension MoyaError {
enum GatewayStatusCode: Int {
//
case success = 0
//
case failure = -1
//
case outdateVersion = 3
/** ============== ============== */

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Rectangle 168@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Rectangle 168@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Group_1672@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Group_1672@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "correct@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "correct@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "error@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "error@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "header_bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "header_bg@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

View File

@ -24,6 +24,8 @@ extension DefaultsKeys {
///
extension Notification.Name {
/// config
static let RefreshUserConfigNotification = Notification.Name("RefreshUserConfigNotification")
///
static let RefreshGroupInfoNotification = Notification.Name("RefreshGroupInfoNotification")
}

View File

@ -337,10 +337,10 @@ extension UIView {
// MARK: -
extension UIView {
func getDateInterval2String(date: String) -> String {
func getDateInterval2String(date: String, dateFormat: String="yyyy.MM.dd HH:mm:ss") -> String {
let date = Date(timeIntervalSince1970: TimeInterval(date.double))
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy.MM.dd HH:mm:ss"
dateFormatter.dateFormat = dateFormat
let dateString = dateFormatter.string(from: date)
return dateString
}

View File

@ -27,6 +27,10 @@ struct UserConfigModel: Mappable {
/// 1 2 3
var vip: Int = 1
///
var vip_name: String = ""
///
var vip_expire_time: String = ""
/// Config
var config: SystemConfigModel?
@ -45,6 +49,8 @@ struct UserConfigModel: Mappable {
head_pic <- (map["head_pic"], kIntTransformStr)
config <- map["config"]
vip <- map["vip"]
vip_name <- map["vip_name"]
vip_expire_time <- map["vip_expire_time"]
}
}

View File

@ -97,7 +97,7 @@ extension ApiManager {
// handleOutdateVersion(message)
case GatewayStatusCode.userLoginExpair.rawValue: // token
handleTokenExpired(msg: message)
case GatewayStatusCode.noAuthority.rawValue: // 退
case GatewayStatusCode.failure.rawValue, GatewayStatusCode.noAuthority.rawValue: // 退
handlePopView(message)
default:
///
@ -138,9 +138,9 @@ extension ApiManager {
private func handleTokenExpired(msg: String?) {
MainAsync {
AppContextManager.shared.deleteAccount()
// IMServiceManager.shared.logout()
GroupIMService.shared.logout()
DLToast.showError(text: msg ?? "登录失效,请重新登录!") {
// AppDelegate.shared.showMainViewController()
AppDelegate.shared.showMainViewController()
}
}
}

View File

@ -31,6 +31,8 @@ enum Route: String {
case groupChat = "groupChat"
///
case groupSetting = "groupSetting"
///
case account = "account"
}
extension Route: RouterTarget {
@ -80,8 +82,8 @@ extension AppRouter: AppRouterProtocol {
return false
}
if vc.isNeedLogin && !AppContextManager.shared.isGuest {
// AppRouter.push(Route.login)
if vc.isNeedLogin && AppContextManager.shared.isGuest {
AppRouter.push(Route.login)
return false
}
@ -122,12 +124,16 @@ extension AppRouter: AppRouterProtocol {
// MARK: -
AppRouter.register(Route.joinGroup) { url, parameters in
JoinGroupVC()
let vc = JoinGroupVC()
vc.isNeedLogin = true
return vc
}
// MARK: -
AppRouter.register(Route.createGroup) { url, parameters in
CreateGroupVC()
let vc = CreateGroupVC()
vc.isNeedLogin = true
return vc
}
// MARK: -
@ -162,6 +168,13 @@ extension AppRouter: AppRouterProtocol {
let groupId = parameters["groupId"].safeString
return GroupSettingVC(groupId: groupId)
}
// MARK: -
AppRouter.register(Route.account) { url, parameters in
let vc = AccountVC()
vc.isNeedLogin = true
return vc
}
}
}

View File

@ -47,13 +47,14 @@ final class GroupChatVC: BaseViewController {
setupMessageListener()
setupVoiceRecording()
setupPanelDismiss()
setupKeyboard()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
IQKeyboardManager.shared.isEnabled = false
IQKeyboardManager.shared.resignOnTouchOutside = false
setupKeyboard()
}
override func viewDidAppear(_ animated: Bool) {
@ -69,6 +70,8 @@ final class GroupChatVC: BaseViewController {
VoicePlayerManager.shared.stop()
IQKeyboardManager.shared.isEnabled = true
IQKeyboardManager.shared.resignOnTouchOutside = true
NotificationCenter.default.removeObserver(self)
}
// MARK: - Keyboard
@ -418,6 +421,7 @@ final class GroupChatVC: BaseViewController {
self.viewModel.memberList = response.list
self.rootView.groupNameLabel.text = model.name
self.rootView.groupAvatarView.image = model.groupIcon
self.rootView.reviewBtn.isHidden = !model.is_owner
self.viewModel.loadMessages()
}.disposed(by: disposeBag)
}
@ -464,9 +468,9 @@ final class GroupChatVC: BaseViewController {
self?.showBigImage(imgUrlList: [msg.imageUrl], currentPage: 0, projectiveView: cell.photoView)
}
return cell
case let .notification(text):
case let .notification(text, showTime, timestamp):
let cell: NotificationMsgCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(text)
cell.configure(text, showTime: showTime, timestamp: timestamp)
return cell
}
}

View File

@ -628,6 +628,32 @@ class TextReceivedMsgCell: UITableViewCell {
// MARK: - cell
final class NotificationMsgCell: UITableViewCell {
func configure(_ text: NSAttributedString, showTime: Bool, timestamp: TimeInterval) {
timeLabel.isHidden = !showTime
timeLabel.text = showTime ? formatTime(timestamp) : nil
contentLabel.attributedText = text
}
private func formatTime(_ t: TimeInterval) -> String {
let date = Date(timeIntervalSince1970: t)
let now = Date()
let calendar = Calendar.current
let f = DateFormatter()
if calendar.isDateInToday(date) { f.dateFormat = "HH:mm" }
else if calendar.isDateInYesterday(date) { f.dateFormat = "'昨天' HH:mm" }
else if calendar.isDate(date, equalTo: now, toGranularity: .year) { f.dateFormat = "M-d HH:mm" }
else { f.dateFormat = "yyyy-M-d HH:mm" }
return f.string(from: date)
}
private let timeLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = UIColor(hexStr: "#999999")
label.textAlignment = .center
return label
}()
private let contentLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
@ -641,18 +667,23 @@ final class NotificationMsgCell: UITableViewCell {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
contentView.addSubview(timeLabel)
contentView.addSubview(contentLabel)
timeLabel.layoutChain
.top(10)
.centerX()
contentLabel.layoutChain
.edges(UIEdgeInsets(top: 10, left: 40, bottom: 10, right: 40))
.topToBottomOfView(timeLabel, offset: 8)
.edgesHorzontal(40)
.bottom(10)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(_ text: String) {
contentLabel.text = text
}
}
// MARK: -
extension UITableViewCell {

View File

@ -20,7 +20,7 @@ enum ChatSectionItem {
case voiceReceived(ChatMessage)
case imageSend(ChatMessage)
case imageReceived(ChatMessage)
case notification(String)
case notification(NSAttributedString, showTime: Bool = false, timestamp: TimeInterval = 0)
}
typealias ChatSectionModel = SectionModel<String, ChatSectionItem>
@ -198,7 +198,7 @@ final class GroupChatViewModel {
case let .send(m), let .received(m), let .emojiSend(m), let .emojiReceived(m),
let .voiceSend(m), let .voiceReceived(m), let .imageSend(m), let .imageReceived(m):
return m.timestamp
case .notification: return 0
case let .notification(_, _, ts): return ts
}
}
@ -212,7 +212,7 @@ final class GroupChatViewModel {
case var .voiceReceived(m):m.showTime = show; return .voiceReceived(m)
case var .imageSend(m): m.showTime = show; return .imageSend(m)
case var .imageReceived(m):m.showTime = show; return .imageReceived(m)
case .notification: return item
case let .notification(text, _, ts): return .notification(text, showTime: show, timestamp: ts)
}
}
@ -278,10 +278,11 @@ final class GroupChatViewModel {
private func toSectionItem(_ msg: OIMMessageInfo) -> ChatSectionItem? {
//
if msg.contentType.rawValue == 1501 || msg.contentType.rawValue == 1510,
let noti = msg.notificationElem {
let text = parseNotification(noti)
if !text.isEmpty { return .notification(text) }
if (msg.contentType.rawValue == 1501 || msg.contentType.rawValue == 1510 || msg.contentType.rawValue == 1520),
let noti = msg.notificationElem,
let text = parseNotification(noti, contentType: msg.contentType.rawValue) {
let ts = TimeInterval(msg.sendTime) / 1000.0
return .notification(text, showTime: false, timestamp: ts)
}
//
if msg.contentType.rawValue == 103 || msg.contentType.rawValue == 104 {
@ -304,17 +305,38 @@ final class GroupChatViewModel {
return chatMsg.isSelf ? .send(chatMsg) : .received(chatMsg)
}
private func parseNotification(_ elem: OIMNotificationElem) -> String {
private func parseNotification(_ elem: OIMNotificationElem, contentType: Int) -> NSAttributedString? {
guard let data = elem.detail?.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let group = json["group"] as? [String: Any],
let _ = json["opUser"] as? [String: Any] else { return "" }
let opUser = json["opUser"] as? [String: Any] else { return nil }
// """XXX"
let ownerID = group["ownerUserID"] as? String ?? ""
let ownerNickName = getUserNickName(id: ownerID)
let displayStr = ownerID == AppContextManager.shared.userId ? "圈子已经创建" : "\(ownerNickName) 创建了圈子"
return displayStr
switch contentType {
case 1501, 1510:
//
let ownerID = group["ownerUserID"] as? String ?? ""
let ownerNickName = getUserNickName(id: ownerID)
let text = ownerID == AppContextManager.shared.userId ? "圈子已经创建" : "\(ownerNickName) 创建了圈子"
return NSAttributedString(string: text)
case 1520:
//
let opUserID = opUser["userID"] as? String ?? ""
let opNickName = getUserNickName(id: opUserID)
let newName = group["groupName"] as? String ?? ""
guard !newName.isEmpty else { return nil }
let tip = "\(opNickName) 将群名称修改为 "
let result = NSMutableAttributedString(string: tip + newName)
result.addAttribute(.font, value: UIFont.systemFont(ofSize: 12), range: NSRange(location: 0, length: result.length))
//
let nameRange = NSRange(location: tip.count, length: newName.utf16.count)
result.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: nameRange)
return result
default:
return nil
}
}
}

View File

@ -12,12 +12,18 @@ final class GroupIMService {
static let shared = GroupIMService()
private var isLogined = false
private var isInited = false
private var isLogining = false
///
private var pendingLoginCompletions: [(Bool) -> Void] = []
private init() {}
// MARK: - Init SDK
func initSDK() {
// SDK
guard !isInited else { return }
isInited = true
let config = OIMInitConfig()
config.apiAddr = URLManager.shared.openIM_API
config.wsAddr = URLManager.shared.openIM_WS
@ -32,30 +38,64 @@ final class GroupIMService {
}
// MARK: - Login
func login(completion: @escaping (Bool) -> Void) {
guard !isLogined, let token = AppContextManager.shared.imToken else {
///
func ensureLogin(completion: @escaping (Bool) -> Void) {
if OIMManager.manager.getLoginStatus() == .logged {
completion(true)
return
}
login(completion: completion)
}
func login(completion: @escaping (Bool) -> Void) {
//
if OIMManager.manager.getLoginStatus() == .logged {
completion(true)
return
}
guard let token = AppContextManager.shared.imToken, !token.isEmpty else {
completion(false)
return
}
let userId = AppContextManager.shared.userId
guard !userId.isEmpty, !token.isEmpty else {
guard !userId.isEmpty else {
completion(false)
return
}
//
pendingLoginCompletions.append(completion)
guard !isLogining else { return }
isLogining = true
OIMManager.manager.login(userId, token: token) { [weak self] _ in
self?.isLogined = true
completion(true)
} onFailure: { code, msg in
self?.finishLogin(success: true)
} onFailure: { [weak self] code, msg in
print("OpenIM login failed: \(code) \(msg ?? "")")
completion(false)
self?.finishLogin(success: false)
}
}
private func finishLogin(success: Bool) {
isLogining = false
let callbacks = pendingLoginCompletions
pendingLoginCompletions.removeAll()
callbacks.forEach { $0(success) }
}
// MARK: - Logout
func logout(completion: ((Bool) -> Void)? = nil) {
OIMManager.manager.logoutWith(onSuccess: { (_: String?) in
completion?(true)
}, onFailure: { (code: Int, msg: String?) in
print("OpenIM logout failed: \(code) \(msg ?? "")")
completion?(false)
})
}
// MARK: - Get Joined Groups
func getJoinedGroups(completion: @escaping ([OIMGroupInfo]) -> Void) {
print("GroupIMService: getJoinedGroups called, isLogined=\(isLogined)")
OIMManager.manager.getJoinedGroupListWith(onSuccess: { groups in
print("GroupIMService: getJoinedGroups success, count=\(groups?.count ?? 0)")
completion(groups ?? [])
}, onFailure: { code, msg in
print("GroupIMService: getJoinedGroups failed: \(code) \(msg ?? "")")
@ -79,6 +119,36 @@ final class GroupIMService {
conversationListener = ConversationListenerProxy(handler: handler)
OIMManager.callbacker.addConversationListener(listener: conversationListener!)
}
// MARK: - Group Listener
private var groupListener: GroupListenerProxy?
/// /退/ SDK
func setGroupListener(_ handler: @escaping () -> Void) {
groupListener = GroupListenerProxy(handler: handler)
OIMManager.callbacker.addGroupListener(listener: groupListener!)
}
}
// MARK: - GroupListenerProxy
private class GroupListenerProxy: NSObject, OIMGroupListener {
private let handler: () -> Void
init(handler: @escaping () -> Void) {
self.handler = handler
}
func onJoinedGroupAdded(_ groupInfo: OIMGroupInfo) {
handler()
}
func onJoinedGroupDeleted(_ groupInfo: OIMGroupInfo) {
handler()
}
func onGroupInfoChanged(_ changeInfo: OIMGroupInfo) {
handler()
}
}
// MARK: - ConversationListenerProxy

View File

@ -84,6 +84,24 @@ class GroupSettingVC: BaseViewController {
guard let model = self.viewModel.groupModel else { return }
AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": model.toJSON()])
}).disposed(by: disposeBag)
//
rootView.dismissGroupView.rx.tapGesture.subscribe(onNext: { _ in
VerificationPopView.show {
}
}).disposed(by: disposeBag)
// 退
rootView.leaveGroupView.rx.tapGesture.subscribe(onNext: { _ in
self.showConfirmPop(title: "确定要离开这个圈子吗?",
message: "您将无法再与此圈子成员分享位置。确定要离开吗?",
confirmText: "",
confirmBlock: { },
cancelText: "") {
self.requestLeaveGroup()
}
}).disposed(by: disposeBag)
}
// MARK: - API
@ -136,6 +154,16 @@ class GroupSettingVC: BaseViewController {
self.requestGroupInfo()
}.disposed(by: disposeBag)
}
private func requestLeaveGroup() {
DLToast.showLoading()
GroupService.leave(requestData: ["group_key": viewModel.groupId]).subscribe { response in
DLToast.dismiss()
DLToast.show(text: "成功退出") {
AppRouter.shared.popToRoot()
}
}.disposed(by: disposeBag)
}
// MARK: - Init
init(groupId: String) {

View File

@ -35,12 +35,35 @@ final class GroupViewController: BaseViewController {
}
// MARK: - OpenIM
private func setupOpenIM() {
GroupIMService.shared.initSDK()
GroupIMService.shared.login { [weak self] success in
guard success else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self?.requestIMGroups(retryCount: 3)
private var hasSetupIMListeners = false
/// IM +
private func loadIMData() {
GroupIMService.shared.ensureLogin { [weak self] success in
guard let self = self else { return }
guard success else {
return
}
self.setupIMListenersIfNeeded()
self.refreshIMData()
}
}
/// IM /SDK /退
private func setupIMListenersIfNeeded() {
guard !hasSetupIMListeners else { return }
hasSetupIMListeners = true
GroupIMService.shared.setConversationListener { [weak self] _ in
GroupIMService.shared.getConversationList { conversations in
self?.viewModel.updateConversations(conversations)
}
}
// /SDK
GroupIMService.shared.setGroupListener { [weak self] in
GroupIMService.shared.getJoinedGroups { groups in
self?.viewModel.loadIMGroups(groups)
}
}
}
@ -63,26 +86,6 @@ final class GroupViewController: BaseViewController {
group.notify(queue: .main) { [weak self] in
DLToast.dismiss()
self?.viewModel.loadIMGroups(groups, conversations: conversations)
// GroupIMService.shared.setConversationListener { [weak self] _ in
// GroupIMService.shared.getConversationList { conversations in
// self?.viewModel.updateConversations(conversations)
// }
// }
}
}
private func requestIMGroups(retryCount: Int = 3) {
DLToast.showLoading(text: "消息获取中...")
GroupIMService.shared.getJoinedGroups { [weak self] groups in
guard let self = self else { return }
if groups.isEmpty && retryCount > 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.requestIMGroups(retryCount: retryCount - 1)
}
} else {
self.refreshIMData()
}
}
}
@ -124,13 +127,7 @@ final class GroupViewController: BaseViewController {
rootView.joinedTabLabel.rx.tapGesture
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(1) })
.disposed(by: disposeBag)
NotificationCenter.default.rx.notification(.RefreshGroupInfoNotification)
.subscribe(onNext: { [weak self] _ in
self?.requestIMGroups()
})
.disposed(by: disposeBag)
Observable.merge(
rootView.createdTableView.rx.modelSelected(GroupCellData.self).asObservable(),
rootView.joinedTableView.rx.modelSelected(GroupCellData.self).asObservable()
@ -140,6 +137,16 @@ final class GroupViewController: BaseViewController {
AppRouter.push(Route.groupChat, userInfo: ["groupId": groupId])
})
.disposed(by: disposeBag)
NotificationCenter.default.rx.notification(.RefreshUserConfigNotification)
.subscribe(onNext: { [weak self] _ in
self?.requestRecommandGroup()
self?.requestGroupInfo()
guard let config = AppContextManager.shared.systemConfig else { return }
self?.rootView.cycleScrollView.imageURLStringsGroup = config.groupBannerList
})
.disposed(by: disposeBag)
}
private func switchToSegment(_ index: Int) {
@ -203,7 +210,7 @@ final class GroupViewController: BaseViewController {
GroupService.groupInfo().subscribe { response in
guard let model = response.model else { return }
self.viewModel.groupList = model.groups
self.setupOpenIM()
self.loadIMData()
}.disposed(by: disposeBag)
}
}

View File

@ -0,0 +1,68 @@
//
// ReviewMemberListVC.swift
// QuickLocation
//
// Created by on 2026/6/10.
//
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
import ObjectMapper
class ReviewMemberListVC: BaseViewController {
fileprivate var rootView: ReviewMemberListView!
override func loadView() {
rootView = ReviewMemberListView(frame: UIScreen.main.bounds)
view = rootView
}
private var viewModel: ReviewMemberListViewModel
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
bindViewModel()
requestReviewlist()
}
private func bindViewModel() {
viewModel.output.sectionedItems
.bind(to: rootView.tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
// MARK: - UITableViewDataSource
lazy private var dataSource: RxTableViewSectionedReloadDataSource<ReviewMemberListSectionModel> = {
return RxTableViewSectionedReloadDataSource<ReviewMemberListSectionModel>(
configureCell: { (_, tableView, indexPath, model) in
let cell: ReviewMemberCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model)
return cell
})
}()
// MARK: - API
private func requestReviewlist() {
DLToast.showLoading()
GroupService.reviewlist(requestData: ["group_key": viewModel.groupId]).subscribe { response in
DLToast.dismiss()
}.disposed(by: disposeBag)
}
// MARK: - Init
init(groupId: String) {
self.viewModel = ReviewMemberListViewModel(groupId: groupId)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,35 @@
//
// ReviewMemberListVM.swift
// QuickLocation
//
// Created by on 2026/6/10.
//
import RxSwift
import RxDataSources
import ObjectMapper
typealias ReviewMemberListSectionModel = SectionModel<String, GroupMemberModel>
struct ReviewMemberListViewModel {
let groupId: String
var groupModel: GroupInfoModel?
struct Output {
var sectionedItems: Observable<[ReviewMemberListSectionModel]>
}
let output: Output
private var disposeBag = DisposeBag()
private let sectionedItems = PublishSubject<[ReviewMemberListSectionModel]>()
init(groupId: String) {
self.groupId = groupId
output = Output(
sectionedItems: sectionedItems.asObservable()
)
}
}

View File

@ -0,0 +1,222 @@
//
// ReviewMemberListView.swift
// QuickLocation
//
// Created by on 2026/6/10.
//
import UIKit
import RxSwift
import RxCocoa
class ReviewMemberListView: UIView {
var disposeBag = DisposeBag()
private func setupRx() {
backBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.shared.popOrDismiss()
}).disposed(by: disposeBag)
}
private func setupUI() {
addSubview(navBgView)
addSubview(navBarView)
navBarView.addSubview(navTitleLabel)
navBarView.addSubview(backBtn)
addSubview(cornerBgView)
addSubview(tableView)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160/375)
navBarView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navTitleLabel.layoutChain
.top(kStatusBarHeight + 12)
.centerY(backBtn)
.centerX()
backBtn.layoutChain
.centerY(navTitleLabel)
.left(15)
.width(24)
.height(24)
cornerBgView.layoutChain
.edges(excludingEdge: .bottom)
.bottom(kSafeBottomMargin + 10, relation: .greaterThanOrEqual)
tableView.layoutChain
.edges(excludingEdge: .bottom)
.bottom(kSafeBottomMargin + 10, relation: .greaterThanOrEqual)
}
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill
return iv
}()
lazy var navBarView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var navTitleLabel: UILabel = {
let label = UILabel()
label.text = "待审核成员"
label.font = .systemFont(ofSize: 18, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.textAlignment = .center
return label
}()
lazy var backBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100)
return btn
}()
lazy var cornerBgView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
view.cornerRadius = 10
return view
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.estimatedRowHeight = 77
tableView.register(ReviewMemberCell.self)
return tableView
}()
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .white
setupUI()
setupRx()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - ReviewMemberCell
class ReviewMemberCell: UITableViewCell {
func configure(_ model: GroupMemberModel) {
}
override init(style: CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
setupSubviews()
}
private func setupSubviews() {
contentView.addSubview(bgView)
bgView.addSubview(avaterImgView)
bgView.addSubview(nameLab)
bgView.addSubview(agreeBtn)
bgView.addSubview(refuseBtn)
bgView.layoutChain
.edgesHorzontal(15)
.edgesVertical()
avaterImgView.layoutChain
.edgesVertical(20)
.left(15)
.height(45)
.widthToHeight(1)
nameLab.layoutChain
.leftToRightOfView(avaterImgView, offset: 10)
.centerY(avaterImgView)
agreeBtn.layoutChain
.centerY()
.right(15)
.width(34)
.height(15)
refuseBtn.layoutChain
.centerY()
.rightToLeftOfView(agreeBtn, offset: 8)
.width(34)
.height(15)
}
lazy var bgView: UIView = {
let view = UIView()
view.backgroundColor = .clear//UIColor(hexStr: "#F5FBFF")
return view
}()
lazy var avaterImgView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.cornerRadius = 22.5
return view
}()
lazy var nameLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#0F2846")
label.font = .systemFont(ofSize: 14, weight: .semibold)
return label
}()
lazy var agreeBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("同意", for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 10, weight: .regular)
btn.setBackgroundColor(UIColor(hexStr: "#16B3FF"), for: .normal)
btn.cornerRadius = 7.5
return btn
}()
lazy var refuseBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("拒绝", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 10, weight: .regular)
btn.setBackgroundColor(.white, for: .normal)
btn.borderWidth = 0.5
btn.borderColor = UIColor(hexStr: "#16B3FF")
btn.cornerRadius = 7.5
return btn
}()
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}

View File

@ -0,0 +1,380 @@
//
// VerificationPopView.swift
// QuickLocation
//
// Created by on 2026/6/10.
//
import UIKit
import RxSwift
import RxCocoa
import IQKeyboardManagerSwift
class VerificationPopView: UIView {
private static let shared = VerificationPopView(frame: CGRect(origin: .zero, size: kScreenSize))
var disposeBag = DisposeBag()
private var completion: (() -> Void)?
static func show(completion: @escaping (() -> Void)) {
guard let superView = kKeyWindow else {
return
}
if VerificationPopView.shared.superview != nil {
VerificationPopView.shared.removeFromSuperview()
VerificationPopView.shared.bgView.frame = .zero
}
VerificationPopView.shared.num1Lab.text = "\(Int.random(in: 1...9))"
VerificationPopView.shared.num2Lab.text = "\(Int.random(in: 1...9))"
VerificationPopView.shared.resultTF.text = ""
VerificationPopView.shared.bgView.alpha = 1
VerificationPopView.shared.bgView.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
superView.addSubview(VerificationPopView.shared)
superView.bringSubviewToFront(VerificationPopView.shared)
UIView.animate(withDuration: 0.25) {
VerificationPopView.shared.bgView.alpha = 1
} completion: { _ in
VerificationPopView.shared.resultTF.becomeFirstResponder()
}
VerificationPopView.shared.completion = {
completion()
VerificationPopView.dismiss()
}
IQKeyboardManager.shared.resignOnTouchOutside = false
}
///
static func dismiss() {
guard VerificationPopView.shared.superview != nil else { return }
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) {
VerificationPopView.shared.bgView.alpha = 0
} completion: { _ in
VerificationPopView.shared.removeFromSuperview()
IQKeyboardManager.shared.resignOnTouchOutside = true
}
}
private func setupRx() {
resultTF.rx.text
.bind(to: resultLab.rx.text)
.disposed(by: disposeBag)
resultTF.rx.text.orEmpty
.subscribe(onNext: { [weak self] text in
guard let self = self,
let num1 = self.num1Lab.text,
let num2 = self.num2Lab.text else { return }
let result = num1.integer + num2.integer
if !text.isEmpty {
self.resultIconView.image = UIImage(named: result == text.integer ? "Popup/correct" : "Popup/error")
self.confirmBtn.isEnabled = result == text.integer
}
else {
self.resultIconView.image = nil
self.confirmBtn.isEnabled = false
}
})
.disposed(by: disposeBag)
confirmBtn.rx.tap.subscribe(onNext: { _ in
self.completion?()
}).disposed(by: disposeBag)
closeBtn.rx.tap.subscribe(onNext: { _ in
VerificationPopView.dismiss()
}).disposed(by: disposeBag)
}
private lazy var bgView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.5)
return view
}()
lazy var popView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var popBgView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 30
return view
}()
lazy var logoImgView: UIImageView = {
let view = UIImageView(image: UIImage(named: "Popup/bell"))
return view
}()
lazy var headerBgImg: UIImageView = {
let view = UIImageView(image: UIImage(named: "Popup/header_bg"))
view.contentMode = .scaleAspectFill
return view
}()
lazy var titleLab: UILabel = {
let label = UILabel()
label.text = "确定要离开这个圈子吗?"
label.font = .systemFont(ofSize: 20, weight: .bold)
label.textColor = UIColor(hexStr: "#1A1A1A")
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
lazy var contentLab: UILabel = {
let label = UILabel()
label.text = "您离开后将解散圈子,确定 要解散吗?"
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#767676")
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
lazy var tipsLab: UILabel = {
let label = UILabel()
label.text = "请验证:填写组合数字,相加后需等于给定数值"
label.font = .systemFont(ofSize: 8, weight: .medium)
label.textColor = UIColor(hexStr: "#999999")
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
lazy var equationView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let addLab = UILabel()
addLab.text = "+"
addLab.font = .systemFont(ofSize: 16, weight: .medium)
addLab.textColor = UIColor(hexStr: "#999999")
let equalLab = UILabel()
equalLab.text = "="
equalLab.font = .systemFont(ofSize: 16, weight: .medium)
equalLab.textColor = UIColor(hexStr: "#999999")
view.addSubview(num1View)
view.addSubview(addLab)
view.addSubview(num2View)
view.addSubview(equalLab)
view.addSubview(resultView)
view.addSubview(resultIconView)
num1View.layoutChain
.left()
.edgesVertical()
.width(30)
.height(30)
addLab.layoutChain
.leftToRightOfView(num1View, offset: 13)
.centerY()
num2View.layoutChain
.topToView(num1View)
.leftToRightOfView(addLab, offset: 13)
.width(30)
.height(30)
equalLab.layoutChain
.leftToRightOfView(num2View, offset: 13)
.centerY()
resultView.layoutChain
.topToView(num1View)
.leftToRightOfView(equalLab, offset: 13)
.height(30)
.width(30)
resultIconView.layoutChain
.leftToRightOfView(resultView, offset: 10)
.centerY()
.right()
return view
}()
lazy var num1View: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#EFF9FF")
view.cornerRadius = 2
view.addSubview(num1Lab)
num1Lab.layoutChain
.edges()
return view
}()
lazy var num1Lab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(hexStr: "#666666")
label.textAlignment = .center
return label
}()
lazy var num2View: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#EFF9FF")
view.cornerRadius = 2
view.addSubview(num2Lab)
num2Lab.layoutChain
.edges()
return view
}()
lazy var num2Lab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = UIColor(hexStr: "#666666")
label.textAlignment = .center
return label
}()
lazy var resultView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#5CBBFF")
view.cornerRadius = 2
view.addSubview(resultTF)
view.addSubview(resultLab)
resultLab.layoutChain
.edges(all: 4)
return view
}()
lazy var resultTF: UITextField = {
let tf = UITextField()
tf.keyboardType = .numberPad
tf.returnKeyType = .done
tf.isHidden = true
return tf
}()
lazy var resultLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = .white//UIColor(hexStr: "#16B3FF")
label.textAlignment = .center
return label
}()
lazy var resultIconView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
return view
}()
lazy var confirmBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.backgroundColor = .clear
btn.isEnabled = false
return btn
}()
lazy var closeBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#0F2846"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 25
return btn
}()
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
addSubview(bgView)
bgView.addSubview(popView)
popView.addSubview(popBgView)
popView.addSubview(logoImgView)
popBgView.addSubview(headerBgImg)
popBgView.addSubview(titleLab)
popBgView.addSubview(contentLab)
popBgView.addSubview(tipsLab)
popBgView.addSubview(equationView)
popBgView.addSubview(confirmBtn)
popBgView.addSubview(closeBtn)
popView.layoutChain
.centerY(self, offset: -40)
.centerX()
.width(315)
popBgView.layoutChain.edges()
logoImgView.layoutChain
.left(16)
.top(-45)
.width(109)
.height(109)
headerBgImg.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(100/315)
titleLab.layoutChain
.top(53)
.centerX()
contentLab.layoutChain
.topToBottomOfView(titleLab, offset: 10)
.edgesHorzontal(20)
tipsLab.layoutChain
.topToBottomOfView(contentLab, offset: 8)
.centerX()
equationView.layoutChain
.topToBottomOfView(tipsLab, offset: 6)
.centerX()
.height(30)
closeBtn.layoutChain
.topToBottomOfView(equationView, offset: 20)
.edgesHorzontal(20)
.height(50)
confirmBtn.layoutChain
.topToBottomOfView(closeBtn, offset: 10)
.edgesHorzontal(20)
.height(50)
.bottom(10)
setupRx()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
}
}

View File

@ -10,6 +10,8 @@ import RxSwift
import RxCocoa
import RxDataSources
import CoreLocation
import SwiftyUserDefaults
#if !targetEnvironment(simulator)
import MAMapKit
#endif
@ -51,8 +53,7 @@ class HomeViewController: BaseViewController {
setupHeading()
reactiveAction()
requestUserInfo()
requestGroupInfo()
requestUserConfig()
}
private func reactiveAction() {
@ -81,6 +82,13 @@ class HomeViewController: BaseViewController {
}
}.disposed(by: disposeBag)
// User Config
NotificationCenter.default.rx.notification(.RefreshUserConfigNotification, object: nil)
.subscribe { [weak self] notification in
self?.requestUserConfig()
}.disposed(by: disposeBag)
//
NotificationCenter.default.rx.notification(.RefreshGroupInfoNotification, object: nil)
.subscribe { [weak self] notification in
self?.requestGroupInfo()
@ -110,6 +118,28 @@ class HomeViewController: BaseViewController {
}()
// MARK: - API
///
func requestUserConfig() {
SystemService.userConfig().subscribe(onNext: { response in
guard let model = response.model else { return }
Defaults[\.loginToken] = model.token
AppContextManager.shared.systemConfig = model.config
self.getUserIMToken()
self.requestUserInfo()
self.requestGroupInfo()
}).disposed(by: disposeBag)
}
/// IM Token
func getUserIMToken() {
UserService.imToken().subscribe(onNext: { response in
guard let data = response.data, let token = data["token"] as? String else { return }
AppContextManager.shared.imToken = token
GroupIMService.shared.login { _ in }
}).disposed(by: disposeBag)
}
private func requestUserInfo() {
UserService.userInfo().subscribe { response in
guard let model = response.model else { return }

View File

@ -29,11 +29,14 @@ class LaunchViewController: BaseViewController {
view.backgroundColor = UIColor(hexStr: "#E0F2FF")
setupLayout()
GroupIMService.shared.initSDK()
experienceBtn.rx.tap.subscribe(onNext: { _ in
// Defaults[\.guideShowVersion] = self.showVersion
AppDelegate.shared.showMainViewController()
}).disposed(by: disposeBag)
getUserConfig()
navigateAfterDelay()
@ -65,11 +68,11 @@ class LaunchViewController: BaseViewController {
///
func getUserConfig() {
SystemService.userConfig().subscribe(onNext: { response in
self.getUserIMToken()
guard let model = response.model else { return }
Defaults[\.loginToken] = model.token
AppContextManager.shared.systemConfig = model.config
//
AppContextManager.shared.saveAccount(model)
// AppContextManager.shared.saveAccount(model)
}, onError: { [weak self] (error) in
DLAlert.show(title: error.localizedDescription,
defaultTitle: "重试") { [weak self] in
@ -79,16 +82,6 @@ class LaunchViewController: BaseViewController {
}).disposed(by: disposeBag)
}
/// IM Token
func getUserIMToken() {
UserService.imToken().subscribe(onNext: { response in
guard let data = response.data, let token = data["token"] as? String else { return }
AppContextManager.shared.imToken = token
// SDK
GroupIMService.shared.initSDK()
}).disposed(by: disposeBag)
}
// MARK: - Init
init() {
super.init(nibName: nil, bundle: nil)

View File

@ -229,6 +229,7 @@ class LoginView: UIView {
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Login/visitor_bg"), for: .normal)
btn.isHidden = true
return btn
}()

View File

@ -5,6 +5,7 @@
import Foundation
import RxSwift
import SwiftyUserDefaults
#if !targetEnvironment(simulator)
import GeYanSdk
#endif
@ -35,17 +36,13 @@ final class LoginViewModel: BaseViewModel {
func loginAction(type: String, data: [String : Any]) {
DLToast.showLoading()
UserService.login(type: type, data: data).subscribe { response in
GroupIMService.shared.logout()
DLToast.dismiss()
guard let model = response.model else { return }
if AppContextManager.shared.saveAccount(model) {
NotificationCenter.default.post(name: .RefreshGroupInfoNotification,
object: nil,
userInfo: nil)
DLToast.showSuccess(text: "登录成功") {
AppRouter.shared.popToRoot()
}
}
else {
DLToast.show(text: "登录失败,请重新尝试")
Defaults[\.loginToken] = model.token
DLToast.showSuccess(text: "登录成功") {
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
AppRouter.shared.popOrDismiss()
}
}.disposed(by: disposeBag)
}

View File

@ -0,0 +1,52 @@
//
// AccountVC.swift
// QuickLocation
//
// Created by on 2026/6/10.
//
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
class AccountVC: BaseViewController {
fileprivate var rootView: AccountView!
override func loadView() {
rootView = AccountView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
reactiveAction()
}
private func reactiveAction() {
rootView.logoutBtn.rx.tap.subscribe(onNext: { _ in
self.showConfirmPop(title: "温馨提醒",
message: "确定要退出账号吗?",
confirmText: "",
confirmBlock: { },
cancelText: "") {
self.requestLogout()
}
}).disposed(by: disposeBag)
}
// MARK: - API
private func requestLogout() {
DLToast.showLoading()
UserService.logout().subscribe { response in
DLToast.dismiss()
AppContextManager.shared.deleteAccount()
GroupIMService.shared.logout()
AppDelegate.shared.showMainViewController()
}.disposed(by: disposeBag)
}
}

View File

@ -0,0 +1,319 @@
//
// AccountView.swift
// QuickLocation
//
// Created by on 2026/6/10.
//
import UIKit
import RxSwift
import RxCocoa
class AccountView: UIView {
var disposeBag = DisposeBag()
private func setupRx() {
backBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.shared.popOrDismiss()
}).disposed(by: disposeBag)
}
private func setupUI() {
addSubview(navBgView)
addSubview(navBarView)
navBarView.addSubview(navTitleLabel)
navBarView.addSubview(backBtn)
addSubview(infoView)
infoView.addSubview(infoStackView)
addSubview(otherInfoView)
otherInfoView.addSubview(otherInfoStackView)
addSubview(logoutBtn)
addSubview(cancelledBtn)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160/375)
navBarView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navTitleLabel.layoutChain
.top(kStatusBarHeight + 12)
.centerY(backBtn)
.centerX()
backBtn.layoutChain
.centerY(navTitleLabel)
.left(15)
.width(24)
.height(24)
infoView.layoutChain
.topToBottomOfView(navBarView, offset: 18)
.edgesHorzontal(16)
infoStackView.layoutChain.edges()
userIdView.layoutChain.height(52)
phoneView.layoutChain.height(52)
otherInfoView.layoutChain
.topToBottomOfView(infoView, offset: 18)
.edgesHorzontal(16)
otherInfoStackView.layoutChain.edges()
versionView.layoutChain.height(52)
clearView.layoutChain.height(52)
logoutBtn.layoutChain
.edgesHorzontal(30)
.bottomToTopOfView(cancelledBtn, offset: -10)
.height(50)
cancelledBtn.layoutChain
.edgesHorzontal(30)
.bottom(kSafeBottomMargin + 10)
.height(50)
}
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill
return iv
}()
lazy var navBarView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var navTitleLabel: UILabel = {
let label = UILabel()
label.text = "账号与安全"
label.font = .systemFont(ofSize: 18, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.textAlignment = .center
return label
}()
lazy var backBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100)
return btn
}()
lazy var infoView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.1).cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 0)
view.layer.shadowOpacity = 1
view.layer.shadowRadius = 10
return view
}()
lazy var infoStackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [userIdView, phoneView])
view.axis = .vertical
view.alignment = .fill
view.distribution = .fill
view.spacing = 0
view.backgroundColor = .white
view.cornerRadius = 10
return view
}()
/// UserID
lazy var userIdView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let titleLab = UILabel()
titleLab.text = "ID"
titleLab.font = .systemFont(ofSize: 14, weight: .medium)
titleLab.textColor = UIColor(hexStr: "#1A1A1A")
view.addSubview(titleLab)
titleLab.layoutChain
.left(15)
.centerY()
let contentLab = UILabel()
contentLab.text = AppContextManager.shared.userId
contentLab.font = .systemFont(ofSize: 14, weight: .medium)
contentLab.textColor = UIColor(hexStr: "#1A1A1A")
view.addSubview(contentLab)
contentLab.layoutChain
.right(15)
.centerY()
let line = UIView()
line.backgroundColor = ThemeManager.shared.color.lineColor
view.addSubview(line)
line.layoutChain
.bottom()
.height(0.5)
.edgesHorzontal(15)
return view
}()
///
lazy var phoneView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let titleLab = UILabel()
titleLab.text = "手机号"
titleLab.font = .systemFont(ofSize: 14, weight: .medium)
titleLab.textColor = UIColor(hexStr: "#1A1A1A")
view.addSubview(titleLab)
titleLab.layoutChain
.left(15)
.centerY()
let arrow = UIImageView(image: UIImage(named: "Group/arrow"))
view.addSubview(arrow)
arrow.layoutChain
.right(15)
.width(14)
.height(14)
.centerY()
view.addSubview(phoneLab)
phoneLab.layoutChain
.rightToLeftOfView(arrow, offset: -8)
.centerY()
return view
}()
lazy var phoneLab: UILabel = {
let lab = UILabel()
lab.text = AppContextManager.shared.userPhone
lab.font = .systemFont(ofSize: 14, weight: .medium)
lab.textColor = UIColor(hexStr: "#1A1A1A")
return lab
}()
//
lazy var otherInfoView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.1).cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 0)
view.layer.shadowOpacity = 1
view.layer.shadowRadius = 10
return view
}()
lazy var otherInfoStackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [versionView, clearView])
view.axis = .vertical
view.alignment = .fill
view.distribution = .fill
view.spacing = 0
view.backgroundColor = .white
view.cornerRadius = 10
return view
}()
///
lazy var versionView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let titleLab = UILabel()
titleLab.text = "当前版本"
titleLab.font = .systemFont(ofSize: 14, weight: .medium)
titleLab.textColor = UIColor(hexStr: "#1A1A1A")
view.addSubview(titleLab)
titleLab.layoutChain
.left(15)
.centerY()
let contentLab = UILabel()
contentLab.text = kAppShortVersion
contentLab.font = .systemFont(ofSize: 14, weight: .medium)
contentLab.textColor = UIColor(hexStr: "#1A1A1A")
view.addSubview(contentLab)
contentLab.layoutChain
.right(15)
.centerY()
let line = UIView()
line.backgroundColor = ThemeManager.shared.color.lineColor
view.addSubview(line)
line.layoutChain
.bottom()
.height(0.5)
.edgesHorzontal(15)
return view
}()
///
lazy var clearView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let titleLab = UILabel()
titleLab.text = "清除缓存"
titleLab.font = .systemFont(ofSize: 14, weight: .medium)
titleLab.textColor = UIColor(hexStr: "#1A1A1A")
view.addSubview(titleLab)
titleLab.layoutChain
.left(15)
.centerY()
let arrow = UIImageView(image: UIImage(named: "Group/arrow"))
view.addSubview(arrow)
arrow.layoutChain
.right(15)
.width(14)
.height(14)
.centerY()
return view
}()
lazy var logoutBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("退出登录", for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setTitleColor(.white, for: .normal)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 25
return btn
}()
lazy var cancelledBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("注销账号", for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
btn.setTitleColor(UIColor(hexStr: "#767676"), for: .normal)
btn.backgroundColor = .clear
return btn
}()
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = UIColor(hexStr: "#FAFAFA")
setupUI()
setupRx()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -28,6 +28,20 @@ final class MineView: UIView {
private let subtitleColor = UIColor(hexStr: "#767676")
private let vipBtnTextColor = UIColor(hexStr: "#00343B")
func setupData(_ model: UserConfigModel) {
guard model.vip > 1 else {
vipTitleLab.text = "开通会员"
vipContentLab.text = "会员尊享10+VIP特权"
vipActivateButton.setTitle("立即开通", for: .normal)
return
}
vipTitleLab.text = model.vip_name
vipContentLab.text = model.vip == 3 ? "会员终身有效"
: "会员到期:\(self.getDateInterval2String(date: model.vip_expire_time, dateFormat: "yyyy.MM.dd"))"
vipActivateButton.setTitle("点击续费", for: .normal)
}
private func setupRx() {
avatarImageView.rx.tapGesture.subscribe { _ in
AppRouter.push(Route.login)
@ -64,7 +78,7 @@ final class MineView: UIView {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .heavy)
label.textColor = titleColor
label.text = "肖战大王叫我来巡山"
label.text = " "
return label
}()
@ -79,7 +93,7 @@ final class MineView: UIView {
let label = UILabel()
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = subtitleColor
label.text = "ID:1234567"
label.text = " "
return label
}()

View File

@ -35,15 +35,16 @@ final class MineViewController: BaseViewController {
bindViewModel()
reactiveAction()
rootView.avatarImageView.image = AppContextManager.shared.avaterIcon
rootView.nameLabel.text = AppContextManager.shared.name
rootView.idLabel.text = "ID:" + AppContextManager.shared.userId
viewModel.loadMenuData()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
rootView.avatarImageView.image = AppContextManager.shared.avaterIcon
rootView.nameLabel.text = AppContextManager.shared.name
rootView.idLabel.text = "ID:" + AppContextManager.shared.userId
requestUserInfo()
}
// MARK: - Bindings
@ -68,7 +69,7 @@ final class MineViewController: BaseViewController {
private func reactiveAction() {
rootView.vipActivateButton.rx.tap
.subscribe(onNext: { [weak self] in
// TODO: Navigate to VIP activation
AppRouter.push(Route.vipRecharge)
})
.disposed(by: disposeBag)
@ -85,10 +86,6 @@ final class MineViewController: BaseViewController {
DLToast.showSuccess(text: "已复制")
})
.disposed(by: disposeBag)
rootView.onMenuTap = { [weak self] index in
self?.handleMenuTap(at: index)
}
}
// MARK: - UITableViewDataSource
@ -100,29 +97,17 @@ final class MineViewController: BaseViewController {
return cell
})
}()
private func handleMenuTap(at index: Int) {
switch index {
case 0:
// TODO: Account & security
break
case 1:
// TODO: Status setting
break
case 2:
// TODO: Emergency contact
break
case 3:
// TODO: Customer service
break
case 4:
// TODO: Privacy & agreement
break
case 5:
// TODO: Permission check
break
default:
break
}
private func requestUserInfo() {
UserService.userInfo().subscribe { response in
guard let model = response.model else { return }
if AppContextManager.shared.saveAccount(model) {
self.rootView.avatarImageView.image = model.userIcon
self.rootView.nameLabel.text = model.name
self.rootView.idLabel.text = "ID:\(model.uid ?? "")"
self.rootView.setupData(model)
}
}.disposed(by: disposeBag)
}
}

View File

@ -35,7 +35,12 @@ class MineViewModel {
lazy var cellAction: Action<MineMenuItem, Void> = { this in
return Action { model in
switch model.title {
case "账户与安全":
AppRouter.push(Route.account)
default:
break
}
return .empty()
}
}(self)

View File

@ -95,4 +95,24 @@ struct GroupService {
.map(GroupInfoResponse.self)
.asObservable()
}
///
/// - Parameters:
/// - requestDatagroup_key
static func reviewlist(requestData: [String: Any]) -> Observable<GroupInfoResponse> {
let api = GroupAPI.operate(opType: "reviewlist", requestData: requestData).multiTarget
return APIProvider.request(token: api)
.map(GroupInfoResponse.self)
.asObservable()
}
/// 退
/// - Parameters:
/// - requestDatagroup_key
static func leave(requestData: [String: Any]) -> Observable<GroupInfoResponse> {
let api = GroupAPI.operate(opType: "leave", requestData: requestData).multiTarget
return APIProvider.request(token: api)
.map(GroupInfoResponse.self)
.asObservable()
}
}

View File

@ -0,0 +1,271 @@
//
// ConfirmPopView.swift
// QuickLocation
//
// Created by on 2026/6/10.
//
import UIKit
import RxSwift
import RxCocoa
import URLNavigator
class ConfirmPopVC: DLCustomPopVC {
var disposeBag = DisposeBag()
// MARK: - Accessor
public var confirmBlock: (() -> Void)?
public var cancelBlock: (() -> Void)?
public var titleText: String? {
didSet {
titleLab.text = titleText
}
}
public var contentText: String? {
didSet {
contentLab.text = contentText
}
}
public var cancelText: String? {
didSet {
closeBtn.setTitle(cancelText, for: .normal)
}
}
public var confirmText: String? {
didSet {
confirmBtn.setTitle(confirmText, for: .normal)
}
}
private func setupSubviews() {
contentBgView.addSubview(logoImgView)
contentView.addSubview(headerBgImg)
if titleText != nil,
titleText?.isEmpty == false {
contentView.addSubview(titleLab)
}
if contentText != nil,
contentText?.isEmpty == false {
contentView.addSubview(contentLab)
}
if cancelText?.isEmpty == false || confirmText?.isEmpty == false {
contentView.addSubview(stackView)
}
closeBtn.isHidden = !(cancelText?.isEmpty == false)
confirmBtn.isHidden = !(confirmText?.isEmpty == false)
}
private func setupLayout() {
logoImgView.layoutChain
.left(16)
.top(-45)
.width(109)
.height(109)
headerBgImg.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(100/315)
var tempView: UIView?
if titleText != nil,
titleText?.isEmpty == false {
titleLab.layoutChain
.top(53)
.edgesHorzontal(20)
tempView = titleLab
}
if contentText != nil,
contentText?.isEmpty == false {
if let tempView = tempView {
contentLab.layoutChain
.edgesHorzontal(20)
.topToBottomOfView(tempView, offset: 20)
} else {
contentLab.layoutChain
.top(53)
.edgesHorzontal(20)
}
tempView = contentLab
}
guard let tempView = tempView else { return }
if cancelText?.isEmpty == false || confirmText?.isEmpty == false {
stackView.layoutChain
.topToBottomOfView(tempView, offset: 30)
.left(20)
.right(20)
.bottom(15)
confirmBtn.layoutChain
.height(50)
closeBtn.layoutChain
.height(50)
}
}
// MARK: - UI
private lazy var bgView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.5)
return view
}()
lazy var popView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var popBgView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 30
return view
}()
lazy var logoImgView: UIImageView = {
let view = UIImageView(image: UIImage(named: "Popup/bell"))
return view
}()
lazy var headerBgImg: UIImageView = {
let view = UIImageView(image: UIImage(named: "Popup/header_bg"))
view.contentMode = .scaleAspectFill
return view
}()
lazy var titleLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 20, weight: .bold)
label.textColor = UIColor(hexStr: "#1A1A1A")
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
lazy var contentLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#767676")
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
lazy var stackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [confirmBtn, closeBtn])
view.spacing = 10
view.axis = .vertical
view.alignment = .fill
view.distribution = .fill
return view
}()
lazy var confirmBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("", for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 25
btn.addTouchBlock { [weak self] _ in
self?.dismiss(animated: true, completion: {
self?.confirmBlock?()
})
}
return btn
}()
lazy var closeBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.backgroundColor = .clear
btn.addTouchBlock { [weak self] _ in
self?.dismiss(animated: true, completion: {
self?.cancelBlock?()
})
}
return btn
}()
// MARK: - Lifecycle
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
popStyle = .center
centerCornerRadius = 30
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
setupSubviews()
setupLayout()
}
}
public extension UIViewController {
///
/// - Parameters:
/// - title:
/// - message:
/// - confirmText:
/// - confirmBlock:
/// - cancelText:
/// - cancelBlock:
func showConfirmPop(showCloseBtn: Bool = true,
title: String?=nil,
message: String?=nil,
confirmText: String?=nil,
confirmBlock: (() -> Void)?=nil,
cancelText: String?=nil,
cancelBlock: (() -> Void)?=nil) {
if title == nil && message == nil { return }
let vc = ConfirmPopVC()
vc.titleText = title
vc.contentText = message
vc.confirmText = confirmText
vc.confirmBlock = confirmBlock
vc.cancelText = cancelText
vc.cancelBlock = cancelBlock
vc.closeBtn.isHidden = !showCloseBtn
vc.dimmingClick = showCloseBtn
present(vc, animated: true)
}
static func showConfirmPop(showCloseBtn: Bool = true,
title: String?=nil,
message: String?=nil,
confirmText: String?=nil,
confirmBlock: (() -> Void)?=nil,
cancelText: String?=nil,
cancelBlock: (() -> Void)?=nil) {
if title == nil && message == nil { return }
let vc = ConfirmPopVC()
vc.titleText = title
vc.contentText = message
vc.confirmText = confirmText
vc.confirmBlock = confirmBlock
vc.cancelText = cancelText
vc.cancelBlock = cancelBlock
vc.closeBtn.isHidden = !showCloseBtn
vc.dimmingClick = showCloseBtn
UIViewController.topMost?.present(vc, animated: true)
}
}

View File

@ -59,7 +59,7 @@ open class DLCustomPopVC: UIViewController {
/// 16relative
open var centerCornerRadius: CGFloat = 16
/// 32relative
open var centerContentInset: CGFloat = 32
open var centerContentInset: CGFloat = 30
// MARK: - Subviews
///
@ -73,6 +73,13 @@ open class DLCustomPopVC: UIViewController {
return view
}()
/// view
public lazy var contentBgView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
///
public lazy var contentView: UIView = {
let view = UIView()
@ -100,19 +107,24 @@ open class DLCustomPopVC: UIViewController {
view.backgroundColor = .clear
view.addSubview(backgroundView)
view.addSubview(contentView)
view.addSubview(contentBgView)
contentBgView.addSubview(contentView)
backgroundView.layoutChain.edges()
if popStyle == .bottom {
contentView.layoutChain
contentBgView.layoutChain
.left()
.right()
.bottom()
contentView.layoutChain.edges()
} else {
contentView.layoutChain
contentBgView.layoutChain
.centerY()
.left(centerContentInset)
.right(centerContentInset)
contentView.layoutChain.edges()
}
}