- 充值活动

- 注册弹窗
- vip弹窗
This commit is contained in:
linshujie 2026-07-03 18:30:23 +08:00
parent a64e1bcc43
commit 4269af8611
46 changed files with 1453 additions and 48 deletions

View File

@ -195,6 +195,9 @@
30A87A6F2FEF7BE40095E7C6 /* SearchLocationResultVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */; };
30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */; };
30ACD54C2FF6336000174861 /* PopupWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD54B2FF6336000174861 /* PopupWindow.swift */; };
30ACD54E2FF7942E00174861 /* PromotionalActivitiesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD54D2FF7942E00174861 /* PromotionalActivitiesVC.swift */; };
30ACD5502FF7943A00174861 /* PromotionalActivitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD54F2FF7943A00174861 /* PromotionalActivitiesView.swift */; };
30ACD67B2FF7B36A00174861 /* PopupQueueManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD67A2FF7B36A00174861 /* PopupQueueManager.swift */; };
30B74B3A2FF2115A00F6744D /* GroupScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B392FF2115A00F6744D /* GroupScheduleView.swift */; };
30B74B3C2FF2117900F6744D /* GroupScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */; };
30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */; };
@ -490,6 +493,9 @@
30A87A6E2FEF7BE40095E7C6 /* SearchLocationResultVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultVC.swift; sourceTree = "<group>"; };
30A87A702FEF7BED0095E7C6 /* SearchLocationResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationResultView.swift; sourceTree = "<group>"; };
30ACD54B2FF6336000174861 /* PopupWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupWindow.swift; sourceTree = "<group>"; };
30ACD54D2FF7942E00174861 /* PromotionalActivitiesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalActivitiesVC.swift; sourceTree = "<group>"; };
30ACD54F2FF7943A00174861 /* PromotionalActivitiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalActivitiesView.swift; sourceTree = "<group>"; };
30ACD67A2FF7B36A00174861 /* PopupQueueManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupQueueManager.swift; sourceTree = "<group>"; };
30B74B392FF2115A00F6744D /* GroupScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleView.swift; sourceTree = "<group>"; };
30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleVC.swift; sourceTree = "<group>"; };
30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVC.swift; sourceTree = "<group>"; };
@ -1344,7 +1350,10 @@
30ACD54A2FF6332200174861 /* PopupWindow */ = {
isa = PBXGroup;
children = (
30ACD67A2FF7B36A00174861 /* PopupQueueManager.swift */,
30ACD54B2FF6336000174861 /* PopupWindow.swift */,
30ACD54D2FF7942E00174861 /* PromotionalActivitiesVC.swift */,
30ACD54F2FF7943A00174861 /* PromotionalActivitiesView.swift */,
);
path = PopupWindow;
sourceTree = "<group>";
@ -1805,6 +1814,7 @@
305A76912FCA8C7000227D26 /* Constant.swift in Sources */,
305A76922FCA8C7000227D26 /* LogUtils.swift in Sources */,
305A76932FCA8C7000227D26 /* AddImageCell.swift in Sources */,
30ACD54E2FF7942E00174861 /* PromotionalActivitiesVC.swift in Sources */,
30EFF3D82FDA8F1000EB35D4 /* EmergencyContactVC.swift in Sources */,
30A87A682FEE86560095E7C6 /* CreateBubblePopView.swift in Sources */,
30A87A4B2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift in Sources */,
@ -1836,6 +1846,7 @@
30D87D052FE1336300E958FD /* NavigationView.swift in Sources */,
30B819F42FF3CE4900FAB693 /* ItineraryTraceVC.swift in Sources */,
305A76A02FCA8C7000227D26 /* TextTableViewCell.swift in Sources */,
30ACD67B2FF7B36A00174861 /* PopupQueueManager.swift in Sources */,
305A76A12FCA8C7000227D26 /* UIButton+RTL.m in Sources */,
30EFF3A62FD7C5AF00EB35D4 /* GroupSettingVC.swift in Sources */,
305A76A22FCA8C7000227D26 /* Array+Extension.swift in Sources */,
@ -2041,6 +2052,7 @@
305A770C2FCA8C7000227D26 /* PageView.swift in Sources */,
305A770D2FCA8C7000227D26 /* PageViewManager.swift in Sources */,
30D891F52FE22E0600E958FD /* OrderAPI.swift in Sources */,
30ACD5502FF7943A00174861 /* PromotionalActivitiesView.swift in Sources */,
307073EA2FD2715A004C37CC /* GroupChatViewModel.swift in Sources */,
305A770E2FCA8C7000227D26 /* DLAlertPopVC.swift in Sources */,
305A770F2FCA8C7000227D26 /* DLCustomPopVC.swift in Sources */,

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 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" : "benefits@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "benefits@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -43,6 +43,7 @@
<key>UIAppFonts</key>
<array>
<string>douyu.otf</string>
<string>zihunbiantaoti-r.ttf</string>
</array>
</dict>
</plist>

View File

@ -63,6 +63,12 @@ struct UserConfigModel: Mappable {
}
struct SystemConfigModel: Mappable {
///
var popupConfig: PopupWindowConfigModel?
///
var timeConfig: PopupWindowTimeModel?
///
var chatWarning: String = ""
@ -76,5 +82,50 @@ struct SystemConfigModel: Mappable {
mutating func mapping(map: Map) {
chatWarning <- map["client.chatwarning", nested: false]
groupBannerList <- map["client.team.ad", nested: false]
popupConfig <- map["client.popup.display", nested: false]
timeConfig <- map["client.time.setting", nested: false]
}
}
struct PopupWindowConfigModel: Mappable {
/// vip
var pay_pop: Bool = false
///
var search_phone: Bool = false
init?(map: Map) {
}
mutating func mapping(map: Map) {
pay_pop <- map["pay_pop"]
search_phone <- map["search_phone"]
}
}
struct PopupWindowTimeModel: Mappable {
/// push
var buy_vip_tip: Int = 0
///
var search_phone: Int = 0
///
var signup_tip: Int = 0
/// vip
var pay_pop_time: Int = 0
/// vip
var min_discount: Double = 0
init?(map: Map) {
}
mutating func mapping(map: Map) {
buy_vip_tip <- map["buy_vip_tip"]
search_phone <- map["search_phone"]
signup_tip <- map["signup_tip"]
pay_pop_time <- map["pay_pop_time"]
min_discount <- map["min_discount"]
}
}

View File

@ -75,6 +75,8 @@ enum Route: String {
case groupMemberList = "groupMemberList"
///
case itineraryTrace = "itineraryTrace"
///
case promotionalActivities = "promotionalActivities"
}
extension Route: RouterTarget {
@ -346,6 +348,11 @@ extension AppRouter: AppRouterProtocol {
ItineraryTraceVC(scheduleJson: parameters["model"].safeDictionary as! [String : Any],
memberInfo: parameters["memberInfo"].safeDictionary as! [String : Any])
}
// MARK: -
AppRouter.register(Route.promotionalActivities) { url, parameters in
PromotionalActivitiesVC()
}
}
}

View File

@ -31,9 +31,9 @@ class HomeViewController: BaseViewController {
private var tableView: UITableView {
rootView.groupMemberView.tableView
}
private var viewModel = HomeViewModel()
private let locationManager = CLLocationManager()
private var currentHeading: Double = 0
private var members: [CircleMember] = []
@ -50,11 +50,12 @@ class HomeViewController: BaseViewController {
private var lastUpdateTimes: [String: Date] = [:]
/// 线
private var offlineCheckTimer: Timer?
private var popupQueue: PopupQueueManager?
private var sosPlayerKey: UInt8 = 0
private var groupRefreshWorkItem: DispatchWorkItem?
private var groupSwitchWorkItem: DispatchWorkItem?
override func loadView() {
#if !targetEnvironment(simulator)
MAMapView.updatePrivacyAgree(.didAgree)
@ -73,16 +74,16 @@ class HomeViewController: BaseViewController {
reactiveAction()
requestUserConfig()
rootView.quickMessageView.tagListView.delegate = self
// MQTT
UIDevice.current.isBatteryMonitoringEnabled = true
startLocationTimer()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
PopupWindow.show(popupType: .register)
}
// MARK: - MQTT
@ -129,34 +130,34 @@ class HomeViewController: BaseViewController {
accuracy: loc.horizontalAccuracy
)
}
// MARK: - Actions
private func reactiveAction() {
//
rootView.bubbleView.rx.tapGesture.subscribe { _ in
AppRouter.push(Route.createBubble)
}.disposed(by: disposeBag)
//
rootView.signInView.rx.tapGesture.subscribe { _ in
let vc = SignInVC(lastLocation: self.lastLocation)
vc.isNeedLogin = true
AppRouter.push(vc)
}.disposed(by: disposeBag)
// SOS
rootView.sosView.rx.tapGesture.subscribe { _ in
let vc = SOSViewController()
vc.isNeedLogin = true
AppRouter.push(vc)
}.disposed(by: disposeBag)
//
rootView.scheduleView.rx.tapGesture.subscribe { _ in
guard let model = self.viewModel.groupModel else { return }
AppRouter.push(Route.groupSchedule, userInfo: ["groupKey": model.default_group_key])
}.disposed(by: disposeBag)
//
rootView.groupView.rx.tapGesture.subscribe { _ in
guard let groupModel = self.viewModel.groupModel else { return }
@ -168,36 +169,36 @@ class HomeViewController: BaseViewController {
self.requestOperateGroup(groupKey: key)
}
}.disposed(by: disposeBag)
//
rootView.groupMemberView.refreshBtn.rx.tap.subscribe(onNext: { _ in
self.requestGroupInfo()
}).disposed(by: disposeBag)
//
rootView.groupMemberView.inviteJoinBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": self.viewModel.groupInfo])
}).disposed(by: disposeBag)
//
rootView.locationView.rx.tapGesture.subscribe { _ in
if let ann = self.currentUserAnnotation {
self.rootView.mapView.setCenter(ann.coordinate, animated: true)
}
}.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()
}.disposed(by: disposeBag)
//
NotificationCenter.default.rx.notification(.ShowMemberLocationNotification, object: nil)
.subscribe { [weak self] notification in
@ -222,7 +223,7 @@ class HomeViewController: BaseViewController {
}
}.disposed(by: self.disposeBag)
}.disposed(by: disposeBag)
//
rootView.onDismissPanel = { [weak self] in
self?.isMemberPanelShown = false
@ -246,12 +247,12 @@ class HomeViewController: BaseViewController {
self.requestSendEmote(emoteIdx: emoteIdx)
}
}
private func bindViewModel() {
viewModel.output.sectionedItems
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
let selectedMember = tableView.rx.modelSelected(GroupMemberModel.self)
.share()
@ -267,7 +268,7 @@ class HomeViewController: BaseViewController {
})
.disposed(by: disposeBag)
}
// MARK: - UITableViewDataSource
lazy private var dataSource: RxTableViewSectionedReloadDataSource<GroupMemberListSectionModel> = {
return RxTableViewSectionedReloadDataSource<GroupMemberListSectionModel>(
@ -281,7 +282,7 @@ class HomeViewController: BaseViewController {
}()
// MARK: - API
///
func requestUserConfig() {
SystemService.userConfig().subscribe(onNext: { response in
@ -295,9 +296,12 @@ class HomeViewController: BaseViewController {
}
//
self.requestNotice()
//
self.popupQueue = PopupQueueManager()
self.popupQueue?.start(from: self)
}).disposed(by: disposeBag)
}
/// IM Token
func getUserIMToken() {
DLToast.showLoading()
@ -309,7 +313,7 @@ class HomeViewController: BaseViewController {
}
}).disposed(by: disposeBag)
}
private func requestUserInfo(completion: (() -> Void)? = nil) {
UserService.userInfo().subscribe { response in
guard let model = response.model else { return }
@ -318,13 +322,13 @@ class HomeViewController: BaseViewController {
completion?()
}.disposed(by: disposeBag)
}
private func requestGroupInfo(isDefaultGroup: Bool=true) {
GroupService.groupInfo().subscribe { response in
guard let model = response.model else { return }
self.viewModel.groupModel = model
NotificationCenter.default.post(name: .RefreshIMGroupListNotification, object: nil)
guard isDefaultGroup else { return }
self.rootView.groupMemberView.setupCountData(self.viewModel.memberCount, 1)
self.rootView.groupNameLab.text = self.viewModel.groupName
@ -332,13 +336,13 @@ class HomeViewController: BaseViewController {
self.refreshMQTTSubscriptions(model.select_group_employee)
}.disposed(by: disposeBag)
}
private func requestOperateGroup(groupKey: String) {
GroupService.operate(opType: "setdefault", requestData: ["group_key" : groupKey]).subscribe { response in
self.requestGroupInfo()
}.disposed(by: disposeBag)
}
private func requestNotice() {
UserService.notice().subscribe { response in
self.rootView.noticeLab.text = ""
@ -354,7 +358,7 @@ class HomeViewController: BaseViewController {
}
}.disposed(by: disposeBag)
}
// MARK: - Map Setup
private func setupMap() {
#if !targetEnvironment(simulator)
@ -429,7 +433,7 @@ class HomeViewController: BaseViewController {
if let currentModel = list.first(where: { $0.user_id == AppContextManager.shared.userId }) {
lastUpdateTime = currentModel.last_active_time
}
// GPS
let me = CircleMember(
id: "current",
@ -459,7 +463,7 @@ class HomeViewController: BaseViewController {
let onlineMembers = newMembers.filter { $0.isOnline }
guard let mapView = rootView.mapView else { return }
let existing = mapView.annotations?.compactMap { $0 as? MemberAnnotation } ?? []
let existingIDs = Set(existing.map { $0.member.id })
let onlineIDs = Set(onlineMembers.map { $0.id })
@ -531,13 +535,13 @@ extension HomeViewController {
else { return }
let userId = topic.replacingOccurrences(of: "smartdrive/", with: "")
switch msg.type {
case "track":
guard let firstPoint = msg.data?.points?.first else { return }
let coord = CLLocationCoordinate2D(latitude: firstPoint.lat, longitude: firstPoint.lon)
guard CLLocationCoordinate2DIsValid(coord) else { return }
// time 线
let nowMs = Date().timeIntervalSince1970 * 1000
let msgTimeMs = Double(firstPoint.time)
@ -583,7 +587,7 @@ extension HomeViewController {
let lastChar = String(index.string.suffix(index.string.count-1))
let parts = gk.components(separatedBy: "/")
let emoteUserId = parts[1]
if firstChar == "1" || firstChar == "2" { //
let emojiFileName = firstChar == "1" ? "normal_" : "fun_"
let nickName = self.viewModel.getUserNickName(id: emoteUserId)
@ -686,7 +690,7 @@ extension HomeViewController {
else {
debounceGroupRefresh()
}
default:
print("📩 未处理 type=\(msg.type ?? "")")
}
@ -724,7 +728,7 @@ extension HomeViewController {
battery: old.battery
)
}
//
guard let mapView = rootView.mapView else { return }
if let oldMember = members.first(where: { $0.id == userId }), oldMember.isOnline {
@ -773,11 +777,11 @@ extension HomeViewController: MAMapViewDelegate {
func mapView(_ mapView: MAMapView!, didUpdate userLocation: MAUserLocation!, updatingLocation: Bool) {
guard updatingLocation, let location = userLocation.location else { return }
//
let strength = gpsSignalStrength(from: location)
rootView.updateGPSSignal(bars: strength.barCount)
//
lastLocation = location
Defaults[\.currentLatitude] = location.coordinate.latitude
@ -808,7 +812,7 @@ extension HomeViewController {
//
sosPlayer?.stop()
sosPlayer = nil
guard let url = Bundle.main.url(forResource: "sos", withExtension: "mp3") ?? Bundle.main.url(forResource: "sos", withExtension: "mp3", subdirectory: "sound") else {
print("❌ SOS: sos.mp3 not found in bundle")
return
@ -841,7 +845,7 @@ extension HomeViewController: TTGTextTagCollectionViewDelegate {
let emoteIdx = "3\(index)".integer
requestSendEmote(emoteIdx: emoteIdx)
}
func requestSendEmote(emoteIdx: Int) {
guard let model = viewModel.groupModel else { return }
DLToast.showLoading()

View File

@ -26,7 +26,7 @@ class MineViewModel {
let menuItems: [MineMenuItem] = [
MineMenuItem(icon: "Mine/account_security", title: "账户与安全"),
MineMenuItem(icon: "Mine/status_setting", title: "状态设置"),
// MineMenuItem(icon: "Mine/status_setting", title: ""),
MineMenuItem(icon: "Mine/emergency_contact", title: "紧急联系人"),
MineMenuItem(icon: "Mine/customer_service", title: "在线客服"),
MineMenuItem(icon: "Mine/privacy_agreement", title: "隐私与协议"),

View File

@ -0,0 +1,145 @@
//
// PopupQueueManager.swift
// QuickLocation
//
// Created by on 2026/7/3.
//
import UIKit
class PopupQueueManager {
private weak var sourceVC: UIViewController?
private var currentStep = 0
private var countdownTimer: Timer?
func start(from vc: UIViewController) {
sourceVC = vc
currentStep = 0
executeNextStep()
}
private func executeNextStep() {
switch currentStep {
case 0:
tryStartPromotionalActivities()
case 1:
tryStartSearchPopup()
case 2:
tryStartRegisterOrVipPopup()
default:
break
}
}
// MARK: - Step 0: PromotionalActivities
private func tryStartPromotionalActivities() {
guard let config = AppContextManager.shared.systemConfig else {
skipToNext()
return
}
guard config.popupConfig?.pay_pop == true,
AppContextManager.shared.vip == 1,
let timeConfig = config.timeConfig else {
skipToNext()
return
}
startCountdown(seconds: timeConfig.pay_pop_time) { [weak self] in
let vc = PromotionalActivitiesVC()
vc.onDismiss = { [weak self] in
self?.skipToNext()
}
self?.push(vc)
}
}
// MARK: - Step 1: Search PopupWindow
private func tryStartSearchPopup() {
guard let config = AppContextManager.shared.systemConfig else {
skipToNext()
return
}
guard config.popupConfig?.search_phone == true,
let timeConfig = config.timeConfig else {
skipToNext()
return
}
startCountdown(seconds: timeConfig.search_phone) { [weak self] in
PopupWindow.show(popupType: .search) {
self?.skipToNext()
}
}
}
// MARK: - Step 2: Register or VIP PopupWindow (mutually exclusive)
private func tryStartRegisterOrVipPopup() {
guard let config = AppContextManager.shared.systemConfig,
let timeConfig = config.timeConfig else {
return
}
if AppContextManager.shared.isGuest {
startCountdown(seconds: timeConfig.signup_tip) { [weak self] in
PopupWindow.show(popupType: .register) {
// done
}
}
} else if AppContextManager.shared.vip == 1 {
startCountdown(seconds: timeConfig.buy_vip_tip) { [weak self] in
PopupWindow.show(popupType: .vip) {
// done
}
}
}
}
// MARK: - Helpers
private func skipToNext() {
currentStep += 1
executeNextStep()
}
private func startCountdown(seconds: Int, onExpired: @escaping () -> Void) {
guard seconds > 0 else {
onExpired()
return
}
countdownTimer?.invalidate()
var remaining = seconds
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
remaining -= 1
if remaining <= 0 {
timer.invalidate()
self?.countdownTimer = nil
onExpired()
}
}
}
private func push(_ vc: UIViewController) {
let topVC = topViewController()
topVC?.navigationController?.pushViewController(vc, animated: true)
}
private func topViewController() -> UIViewController? {
guard let rootVC = kKeyWindow?.rootViewController else { return nil }
return topViewController(from: rootVC)
}
private func topViewController(from root: UIViewController) -> UIViewController {
if let presented = root.presentedViewController {
return topViewController(from: presented)
}
if let nav = root as? UINavigationController, let visible = nav.visibleViewController {
return topViewController(from: visible)
}
if let tab = root as? UITabBarController, let selected = tab.selectedViewController {
return topViewController(from: selected)
}
return root
}
}

View File

@ -42,14 +42,43 @@ class PopupWindow: UIView {
slideInRegisterPopup()
}
case .vip:
slideInUnlockVip1Popup()
let roll = Int.random(in: 0...3)
switch roll {
case 0:
unlockVip2View.isHidden = true
unlockVip3View.isHidden = true
unlockVip4View.isHidden = true
unlockVip1View.isHidden = false
slideInUnlockVip1Popup()
case 1:
unlockVip1View.isHidden = true
unlockVip3View.isHidden = true
unlockVip4View.isHidden = true
unlockVip2View.isHidden = false
slideInUnlockVip2Popup()
case 2:
unlockVip1View.isHidden = true
unlockVip2View.isHidden = true
unlockVip4View.isHidden = true
unlockVip3View.isHidden = false
slideInUnlockVip3Popup()
default:
unlockVip1View.isHidden = true
unlockVip2View.isHidden = true
unlockVip3View.isHidden = true
unlockVip4View.isHidden = false
slideInUnlockVip4Popup()
}
default:
break
}
}
}
static func show(popupType: PopupType) {
private static var dismissCompletion: (() -> Void)?
static func show(popupType: PopupType, dismissCompletion: (() -> Void)? = nil) {
Self.dismissCompletion = dismissCompletion
guard let superView = kKeyWindow else {
return
}
@ -76,9 +105,15 @@ class PopupWindow: UIView {
stopRegisterStarFlash()
stopRegister1LottieAlternate()
stopUnlockVip1Animations()
stopUnlockVip2Animations()
stopUnlockVip3Animations()
stopUnlockVip4Animations()
registerPopupView.transform = .identity
register2PopupView.transform = .identity
unlockVip1View.transform = .identity
unlockVip2View.transform = .identity
unlockVip3View.transform = .identity
unlockVip4View.transform = .identity
registerStar1.transform = .identity
// register2Star1.transform = .identity
@ -86,6 +121,9 @@ class PopupWindow: UIView {
registerPopupView.isHidden = true
register2PopupView.isHidden = true
unlockVip1View.isHidden = true
unlockVip2View.isHidden = true
unlockVip3View.isHidden = true
unlockVip4View.isHidden = true
bgView.isUserInteractionEnabled = true
}
@ -98,6 +136,9 @@ class PopupWindow: UIView {
} completion: { _ in
PopupWindow.shared.resetPopupState()
PopupWindow.shared.removeFromSuperview()
let completion = Self.dismissCompletion
Self.dismissCompletion = nil
completion?()
}
}
@ -139,6 +180,42 @@ class PopupWindow: UIView {
self?.slideOutUnlockVip1Popup()
})
.disposed(by: disposeBag)
unlockVip2CloseBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.slideOutUnlockVip2Popup()
})
.disposed(by: disposeBag)
unlockVip2Btn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.slideOutUnlockVip2Popup()
})
.disposed(by: disposeBag)
unlockVip3CloseBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.slideOutUnlockVip3Popup()
})
.disposed(by: disposeBag)
unlockVip3Btn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.slideOutUnlockVip3Popup()
})
.disposed(by: disposeBag)
unlockVip4CloseBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.slideOutUnlockVip4Popup()
})
.disposed(by: disposeBag)
unlockVip4Btn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.slideOutUnlockVip4Popup()
})
.disposed(by: disposeBag)
}
private lazy var bgView: UIView = {
@ -155,6 +232,10 @@ class PopupWindow: UIView {
let bgImgView = UIImageView(image: UIImage(named: "PopupWindow/search_bg"))
bgImgView.contentMode = .scaleAspectFill
bgImgView.rx.tapGesture.subscribe(onNext: { _ in
PopupWindow.dismiss()
AppRouter.push(Route.searchLocation)
}).disposed(by: disposeBag)
view.addSubview(bgImgView)
bgImgView.layoutChain
.edges(excludingEdge: .bottom)
@ -270,7 +351,7 @@ class PopupWindow: UIView {
view.addSubview(vip1CloseBtn)
vip1CloseBtn.layoutChain
.topToView(bgImg, offset: 32)
.topToView(bgImg, offset: 16)
.right(17)
view.addSubview(vip1UnlockBtn)
@ -392,6 +473,475 @@ class PopupWindow: UIView {
}
}
// MARK: - 2
private let unlockVip2PopupHeight: CGFloat = 412
lazy var unlockVip2View: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.isHidden = true
let bgImg = UIImageView(image: UIImage(named: "PopupWindow/vip2_bg"))
bgImg.contentMode = .scaleAspectFill
view.addSubview(bgImg)
bgImg.layoutChain
.top(84)
.edgesHorzontal()
.bottom()
let titleLab = UILabel()
let text = "会员升级人次"
let attr = NSMutableAttributedString(string: text)
let strokeAttrs: [NSAttributedString.Key: Any] = [
.strokeWidth: -5,
.strokeColor: UIColor.black.cgColor,
.foregroundColor: UIColor.white,
.font: UIFont(name: "zihunbiantaoti", size: 20) ?? .systemFont(ofSize: 20)
]
attr.setAttributes(strokeAttrs, range: NSRange(location: 0, length: text.count))
titleLab.attributedText = attr
view.addSubview(titleLab)
titleLab.layoutChain
.top(106)
.left(29)
let countTitleLab = UILabel()
let countText = "已突破"
let countAttr = NSMutableAttributedString(string: countText)
let countStrokeAttrs: [NSAttributedString.Key: Any] = [
.strokeWidth: -5,
.strokeColor: UIColor.black.cgColor,
.foregroundColor: UIColor.white,
.font: UIFont(name: "zihunbiantaoti", size: 22) ?? .systemFont(ofSize: 22)
]
countAttr.setAttributes(countStrokeAttrs, range: NSRange(location: 0, length: countText.count))
countTitleLab.attributedText = countAttr
view.addSubview(countTitleLab)
countTitleLab.layoutChain
.topToBottomOfView(titleLab, offset: 22)
.leftToView(titleLab)
view.addSubview(unlockVip2CountLab)
unlockVip2CountLab.layoutChain
.leftToRightOfView(countTitleLab)
.bottomToView(countTitleLab, offset: 3)
view.addSubview(unlockVip2LogoImg)
unlockVip2LogoImg.layoutChain
.top()
.right()
.width(178)
.height(199)
let textImg = UIImageView(image: UIImage(named: "PopupWindow/vip2_text"))
view.addSubview(textImg)
textImg.layoutChain
.topToBottomOfView(unlockVip2LogoImg, offset: 15)
.centerX()
.width(275)
.height(41)
view.addSubview(unlockVip2CloseBtn)
unlockVip2CloseBtn.layoutChain
.bottom(kSafeBottomMargin + 15)
.left(30)
.height(50)
view.addSubview(unlockVip2Btn)
unlockVip2Btn.layoutChain
.bottomToView(unlockVip2CloseBtn)
.right(30)
.height(50)
.widthToView(unlockVip2CloseBtn)
unlockVip2CloseBtn.layoutChain.rightToLeftOfView(unlockVip2Btn, offset: -10)
return view
}()
lazy var unlockVip2LogoImg: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "PopupWindow/vip2_logo")
view.contentMode = .scaleAspectFill
return view
}()
private var unlockVip2CountTarget: Int = 0
private var unlockVip2CountTimer: Timer?
lazy var unlockVip2CountLab: UILabel = {
let label = UILabel()
unlockVip2CountTarget = Int.random(in: 10000...15000)
let text = "0+"
let attr = NSMutableAttributedString(string: text)
let strokeAttrs: [NSAttributedString.Key: Any] = [
.strokeWidth: -5,
.strokeColor: UIColor.black.cgColor,
.foregroundColor: UIColor(hexStr: "#7CE7FC"),
.font: UIFont(name: "zihunbiantaoti", size: 34) ?? .systemFont(ofSize: 34)
]
attr.setAttributes(strokeAttrs, range: NSRange(location: 0, length: text.count))
label.attributedText = attr
return label
}()
lazy var unlockVip2CloseBtn: 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 = .white
btn.cornerRadius = 25
btn.borderWidth = 0.5
btn.borderColor = UIColor(hexStr: "#16B3FF")
return btn
}()
lazy var unlockVip2Btn: 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
return btn
}()
private func startUnlockVip2Animations() {
stopUnlockVip2Animations()
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = -0.08
animation.toValue = 0.08
animation.duration = 0.7
animation.autoreverses = true
animation.repeatCount = .infinity
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
unlockVip2LogoImg.layer.add(animation, forKey: "unlockVip2LogoSwing")
startUnlockVip2CountAnimation()
}
private func stopUnlockVip2Animations() {
unlockVip2LogoImg.layer.removeAnimation(forKey: "unlockVip2LogoSwing")
unlockVip2CountTimer?.invalidate()
unlockVip2CountTimer = nil
}
private func startUnlockVip2CountAnimation() {
unlockVip2CountTimer?.invalidate()
let target = unlockVip2CountTarget
let duration: TimeInterval = 2.0
let interval: TimeInterval = 0.025
let totalSteps = Int(duration / interval)
var step = 0
unlockVip2CountTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] timer in
step += 1
let progress = min(Double(step) / Double(totalSteps), 1.0)
let eased = 1.0 - pow(1.0 - progress, 3)
let current = Int(Double(target) * eased)
let text = "\(current)+"
let attr = NSMutableAttributedString(string: text)
let strokeAttrs: [NSAttributedString.Key: Any] = [
.strokeWidth: -3,
.strokeColor: UIColor.black.cgColor,
.foregroundColor: UIColor(hexStr: "#7CE7FC"),
.font: UIFont(name: "zihunbiantaoti", size: 34) ?? .systemFont(ofSize: 34)
]
attr.setAttributes(strokeAttrs, range: NSRange(location: 0, length: text.count))
self?.unlockVip2CountLab.attributedText = attr
if progress >= 1.0 {
timer.invalidate()
self?.unlockVip2CountTimer = nil
}
}
}
private func slideInUnlockVip2Popup() {
stopUnlockVip2Animations()
unlockVip2CountTarget = Int.random(in: 10000...15000)
let attr = NSMutableAttributedString(string: "0+")
let strokeAttrs: [NSAttributedString.Key: Any] = [
.strokeWidth: -3,
.strokeColor: UIColor.black.cgColor,
.foregroundColor: UIColor(hexStr: "#7CE7FC"),
.font: UIFont(name: "zihunbiantaoti", size: 34) ?? .systemFont(ofSize: 34)
]
attr.setAttributes(strokeAttrs, range: NSRange(location: 0, length: 2))
unlockVip2CountLab.attributedText = attr
bgView.isUserInteractionEnabled = false
unlockVip2View.isHidden = false
unlockVip2View.layoutIfNeeded()
unlockVip2View.transform = CGAffineTransform(translationX: 0, y: unlockVip2PopupHeight)
UIView.animate(withDuration: 0.45, delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.5,
options: .curveEaseOut) {
self.unlockVip2View.transform = .identity
} completion: { _ in
self.bgView.isUserInteractionEnabled = true
self.startUnlockVip2Animations()
}
}
private func slideOutUnlockVip2Popup() {
stopUnlockVip2Animations()
bgView.isUserInteractionEnabled = false
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) {
self.unlockVip2View.transform = CGAffineTransform(translationX: 0, y: self.unlockVip2PopupHeight)
} completion: { _ in
Self.dismiss()
}
}
// MARK: - 3
private let unlockVip3PopupHeight: CGFloat = 385
lazy var unlockVip3View: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.isHidden = true
let bgImg = UIImageView(image: UIImage(named: "PopupWindow/vip3_bg"))
bgImg.contentMode = .scaleAspectFill
view.addSubview(bgImg)
bgImg.layoutChain
.top(45)
.edgesHorzontal()
.bottom()
view.addSubview(unlockVip3LottieView)
unlockVip3LottieView.layoutChain
.top(-120)
.right(-110)
.width(420)
.height(370)
view.addSubview(unlockVip3CloseBtn)
unlockVip3CloseBtn.layoutChain
.bottom(kSafeBottomMargin + 15)
.left(30)
.height(50)
view.addSubview(unlockVip3Btn)
unlockVip3Btn.layoutChain
.bottomToView(unlockVip3CloseBtn)
.right(30)
.height(50)
.widthToView(unlockVip3CloseBtn)
unlockVip3CloseBtn.layoutChain.rightToLeftOfView(unlockVip3Btn, offset: -10)
return view
}()
lazy var unlockVip3LottieView: LottieAnimationView = {
let view = LottieAnimationView(name: "dialog_buy_vip_3")
view.loopMode = .loop
return view
}()
lazy var unlockVip3CloseBtn: 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 = .white
btn.cornerRadius = 25
btn.borderWidth = 0.5
btn.borderColor = UIColor(hexStr: "#16B3FF")
return btn
}()
lazy var unlockVip3Btn: 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
return btn
}()
private func stopUnlockVip3Animations() {
unlockVip3LottieView.stop()
}
private func slideInUnlockVip3Popup() {
stopUnlockVip3Animations()
bgView.isUserInteractionEnabled = false
unlockVip3View.isHidden = false
unlockVip3View.layoutIfNeeded()
unlockVip3View.transform = CGAffineTransform(translationX: 0, y: unlockVip3PopupHeight)
UIView.animate(withDuration: 0.45, delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.5,
options: .curveEaseOut) {
self.unlockVip3View.transform = .identity
} completion: { _ in
self.bgView.isUserInteractionEnabled = true
self.unlockVip3LottieView.play()
}
}
private func slideOutUnlockVip3Popup() {
stopUnlockVip3Animations()
bgView.isUserInteractionEnabled = false
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) {
self.unlockVip3View.transform = CGAffineTransform(translationX: 0, y: self.unlockVip3PopupHeight)
} completion: { _ in
Self.dismiss()
}
}
// MARK: - 4
private let unlockVip4PopupHeight: CGFloat = 368
lazy var unlockVip4View: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.isHidden = true
let bgImg = UIImageView(image: UIImage(named: "PopupWindow/vip4_bg"))
bgImg.contentMode = .scaleAspectFill
view.addSubview(bgImg)
bgImg.layoutChain
.top()
.edgesHorzontal()
.bottom()
view.addSubview(unlockVip4AnimateView)
unlockVip4AnimateView.layoutChain
.top(143)
.rightToCenterXOfView(view, offset: 50)
.width(60)
.height(52)
let label = UILabel()
label.text = "终身会员"
label.font = .systemFont(ofSize: 14, weight: .medium)
view.addSubview(label)
label.layoutChain
.leftToRightOfView(unlockVip4AnimateView, offset: 17)
.bottomToView(unlockVip4AnimateView)
view.addSubview(unlockVip4DiscountLab)
unlockVip4DiscountLab.layoutChain
.bottomToView(label, offset: 10)
.leftToRightOfView(label)
let unitLab = UILabel()
unitLab.text = ""
unitLab.font = .systemFont(ofSize: 14, weight: .medium)
view.addSubview(unitLab)
unitLab.layoutChain
.bottomToView(label, offset: 0)
.leftToRightOfView(unlockVip4DiscountLab)
view.addSubview(unlockVip4CloseBtn)
unlockVip4CloseBtn.layoutChain
.top()
.right(18)
.width(22)
.height(22)
view.addSubview(unlockVip4Btn)
unlockVip4Btn.layoutChain
.centerX()
.width(224)
.height(50)
.bottom(49)
return view
}()
lazy var unlockVip4AnimateView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "PopupWindow/vip4_animate")
return view
}()
lazy var unlockVip4DiscountLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 44, weight: .bold)
label.textColor = UIColor(hexStr: "#FF4E41")
return label
}()
lazy var unlockVip4CloseBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "PopupWindow/close"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 18)
return btn
}()
lazy var unlockVip4Btn: 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
return btn
}()
private func startUnlockVip4Animations() {
stopUnlockVip4Animations()
let animateScale = CABasicAnimation(keyPath: "transform.scale")
animateScale.fromValue = 1.0
animateScale.toValue = 1.15
animateScale.duration = 0.6
animateScale.autoreverses = true
animateScale.repeatCount = .infinity
animateScale.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
unlockVip4AnimateView.layer.add(animateScale, forKey: "unlockVip4AnimateScale")
let btnScale = CABasicAnimation(keyPath: "transform.scale")
btnScale.fromValue = 1.0
btnScale.toValue = 1.08
btnScale.duration = 0.8
btnScale.autoreverses = true
btnScale.repeatCount = .infinity
btnScale.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
unlockVip4Btn.layer.add(btnScale, forKey: "unlockVip4BtnScale")
}
private func stopUnlockVip4Animations() {
unlockVip4AnimateView.layer.removeAnimation(forKey: "unlockVip4AnimateScale")
unlockVip4Btn.layer.removeAnimation(forKey: "unlockVip4BtnScale")
}
private func slideInUnlockVip4Popup() {
if let systemConfig = AppContextManager.shared.systemConfig,
let timeConfig = systemConfig.timeConfig {
unlockVip4DiscountLab.text = (String(format: "%.1f", timeConfig.min_discount/10))
}
stopUnlockVip4Animations()
bgView.isUserInteractionEnabled = false
unlockVip4View.isHidden = false
unlockVip4View.layoutIfNeeded()
unlockVip4View.transform = CGAffineTransform(translationX: 0, y: unlockVip4PopupHeight)
UIView.animate(withDuration: 0.45, delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.5,
options: .curveEaseOut) {
self.unlockVip4View.transform = .identity
} completion: { _ in
self.bgView.isUserInteractionEnabled = true
self.startUnlockVip4Animations()
}
}
private func slideOutUnlockVip4Popup() {
stopUnlockVip4Animations()
bgView.isUserInteractionEnabled = false
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) {
self.unlockVip4View.transform = CGAffineTransform(translationX: 0, y: self.unlockVip4PopupHeight)
} completion: { _ in
Self.dismiss()
}
}
// MARK: - 1
lazy var registerPopupView: UIView = {
let view = UIView()
@ -423,7 +973,7 @@ class PopupWindow: UIView {
.width(272)
.height(50)
.centerX()
view.addSubview(register1UpLottieView)
register1UpLottieView.layoutChain
.bottomToTopOfView(logoImg, offset: 120)
@ -789,6 +1339,9 @@ class PopupWindow: UIView {
addSubview(bgView)
bgView.addSubview(searchPopupView)
bgView.addSubview(unlockVip1View)
bgView.addSubview(unlockVip2View)
bgView.addSubview(unlockVip3View)
bgView.addSubview(unlockVip4View)
bgView.addSubview(registerPopupView)
bgView.addSubview(register2PopupView)
@ -811,6 +1364,22 @@ class PopupWindow: UIView {
.bottom()
.height(unlockVip1PopupHeight)
unlockVip2View.layoutChain
.edgesHorzontal()
.bottom()
.height(unlockVip2PopupHeight)
unlockVip3View.layoutChain
.edgesHorzontal()
.bottom()
.height(unlockVip3PopupHeight)
unlockVip4View.layoutChain
.edgesHorzontal()
.bottom()
.height(unlockVip4PopupHeight)
setupRx()
}

View File

@ -0,0 +1,45 @@
//
// PromotionalActivitiesVC.swift
// QuickLocation
//
// Created by on 2026/7/3.
//
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
class PromotionalActivitiesVC: BaseViewController {
fileprivate var rootView: PromotionalActivitiesView!
var onDismiss: (() -> Void)?
override func loadView() {
rootView = PromotionalActivitiesView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
requestRechargeInfo()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isMovingFromParent {
onDismiss?()
}
}
// MARK: - API
private func requestRechargeInfo() {
DLToast.showLoading()
OrderService.rechargeInfo(type: "member").subscribe(onNext: { [weak self] response in
guard let self = self else { return }
self.rootView.startCountdown(endTime: 1769875199)
}).disposed(by: disposeBag)
}
}

View File

@ -0,0 +1,321 @@
//
// PromotionalActivitiesView.swift
// QuickLocation
//
// Created by on 2026/7/3.
//
import UIKit
import RxSwift
import RxCocoa
class PromotionalActivitiesView: UIView {
var disposeBag = DisposeBag()
private func setupRx() {
backBtn.rx.tap.subscribe(onNext: { _ in
VipWaivePopView.show()
}).disposed(by: disposeBag)
}
private func setupUI() {
addSubview(scrollView)
addSubview(navBarView)
navBarView.addSubview(navTitleLabel)
addSubview(backBtn)
navBarView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navTitleLabel.layoutChain
.top(kStatusBarHeight + 12)
.centerY(backBtn)
.centerX()
backBtn.layoutChain
.top(kStatusBarHeight + 12)
.left(15)
.width(24)
.height(24)
addSubview(bottomView)
bottomView.layoutChain
.edges(excludingEdge: .top)
.height(120)
scrollView.layoutChain
.edges(excludingEdge: .bottom)
.bottomToTopOfView(bottomView)
}
lazy var navBarView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.alpha = 0
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: 15, right: 100)
return btn
}()
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.backgroundColor = UIColor(hexStr: "#F6F9FB")
view.showsVerticalScrollIndicator = false
view.delegate = self
view.bounces = false
view.contentInsetAdjustmentBehavior = .never
view.addSubview(scrollContentView)
scrollContentView.layoutChain
.edges().widthToView(view)
let headerBgImgView = UIImageView(image: UIImage(named: "PromotionalActivities/header_bg"))
headerBgImgView.contentMode = .scaleAspectFill
scrollContentView.addSubview(headerBgImgView)
headerBgImgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(285/375)
let infoView = UIView()
infoView.backgroundColor = UIColor(hexStr: "#F6F9FB")
infoView.layer.cornerRadius = 30
scrollContentView.addSubview(infoView)
infoView.layoutChain
.topToBottomOfView(headerBgImgView, offset: -65)
.edges(excludingEdge: .top)
let iconView = UIImageView(image: UIImage(named: "PromotionalActivities/vip_icon"))
infoView.addSubview(iconView)
iconView.layoutChain
.top(20)
.left(15)
.width(20)
.heightToWidth(1)
let titleLab = UILabel()
titleLab.text = "即刻加入会员"
titleLab.font = .systemFont(ofSize: 16, weight: .semibold)
titleLab.textColor = UIColor(hexStr: "#333333")
infoView.addSubview(titleLab)
titleLab.layoutChain
.leftToRightOfView(iconView, offset: 4)
.centerY(iconView)
let agreementView = UIView()
infoView.addSubview(agreementView)
agreementView.layoutChain
.topToBottomOfView(titleLab, offset: 18)
.left(15)
.right(15)
.height(30)
agreementView.addSubview(agreementCheckBtn)
agreementCheckBtn.layoutChain
.left(0)
.centerY()
.width(16)
.height(16)
agreementView.addSubview(agreementTextView)
agreementTextView.layoutChain
.leftToRightOfView(agreementCheckBtn, offset: 6)
.right(0)
.centerY()
.bottom(0)
let benefitsImgView = UIImageView(image: UIImage(named: "PromotionalActivities/benefits"))
benefitsImgView.contentMode = .scaleAspectFill
infoView.addSubview(benefitsImgView)
benefitsImgView.layoutChain
.topToBottomOfView(agreementView, offset: 20)
.edgesHorzontal(15)
.bottom(15, relation: .greaterThanOrEqual)
.heightToWidth(148/375)
return view
}()
lazy var scrollContentView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var bottomView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.addSubview(countdownLabel)
countdownLabel.layoutChain
.top(11)
.centerX()
view.addSubview(confirmBtn)
confirmBtn.layoutChain
.edgesHorzontal(30)
.bottom(35)
.height(50)
return view
}()
lazy var agreementCheckBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/checkbox"), for: .normal)
btn.setImage(UIImage(named: "Common/checkbox_on"), for: .selected)
btn.rx.tap.subscribe(onNext: { [weak btn] in
btn?.isSelected.toggle()
}).disposed(by: disposeBag)
return btn
}()
lazy var agreementTextView: UITextView = {
let tv = UITextView()
tv.isEditable = false
tv.isScrollEnabled = false
tv.backgroundColor = .clear
tv.textContainerInset = .zero
tv.textContainer.lineFragmentPadding = 0
tv.delegate = self
let text = "购买前请先阅读隐私政策和服务条款"
let attr = NSMutableAttributedString(string: text, attributes: [
.font: UIFont.systemFont(ofSize: 12),
.foregroundColor: UIColor(hexStr: "#999999")
])
let privacyRange = (text as NSString).range(of: "隐私政策")
let termsRange = (text as NSString).range(of: "服务条款")
let linkColor = UIColor(hexStr: "#16B3FF")
attr.addAttributes([.foregroundColor: linkColor, .link: URLManager.shared.privacyPolicyUrl], range: privacyRange)
attr.addAttributes([.foregroundColor: linkColor, .link: URLManager.shared.userAgreementUrl], range: termsRange)
tv.attributedText = attr
tv.linkTextAttributes = [.foregroundColor: linkColor]
return tv
}()
lazy var countdownLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
return label
}()
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
return btn
}()
private var countdownEndTime: TimeInterval = 0
private var countdownTimer: Timer?
func startCountdown(endTime: TimeInterval) {
countdownEndTime = endTime
updateCountdown()
countdownTimer?.invalidate()
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.updateCountdown()
}
}
private func stopCountdown() {
countdownTimer?.invalidate()
countdownTimer = nil
}
private func updateCountdown() {
let remaining = max(0, countdownEndTime - Date().timeIntervalSince1970)
let totalSeconds = Int(remaining)
let days = totalSeconds / 86400
let hours = (totalSeconds % 86400) / 3600
let minutes = (totalSeconds % 3600) / 60
let seconds = totalSeconds % 60
let attr = NSMutableAttributedString()
let normalStyle: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor(hexStr: "#FD501B"),
.font: UIFont.systemFont(ofSize: 12)
]
let timeStyle: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.font: UIFont.systemFont(ofSize: 12),
.backgroundColor: UIColor(hexStr: "#FD501B")
]
attr.append(NSAttributedString(string: "优惠活动倒计时 ", attributes: normalStyle))
let timeStr: String
if days > 0 {
timeStr = String(format: "%d天%02d:%02d:%02d", days, hours, minutes, seconds)
} else {
timeStr = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
}
attr.append(NSAttributedString(string: timeStr, attributes: timeStyle))
attr.append(NSAttributedString(string: " 限时特惠", attributes: normalStyle))
countdownLabel.attributedText = attr
}
override func layoutSubviews() {
super.layoutSubviews()
}
override func willMove(toSuperview newSuperview: UIView?) {
super.willMove(toSuperview: newSuperview)
if newSuperview == nil {
stopCountdown()
}
}
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: - UIScrollViewDelegate
extension PromotionalActivitiesView: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let maxY = scrollView.contentOffset.y
let alpha = maxY / kNaviHeight
navBarView.alpha = alpha < 0.0 ? 0.0 : alpha
}
}
// MARK: - UITextViewDelegate
extension PromotionalActivitiesView: UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
AppRouter.push(Route.web, userInfo: ["url": URL.absoluteString])
return false
}
}

View File

@ -95,9 +95,7 @@ class VipRechargeView: UIView {
.widthToView(scrollView)
headerBgImgView.layoutChain
.top(-kStatusBarHeight)
.edgesHorzontal()
// .edges(excludingEdge: .bottom)
.edges(excludingEdge: .bottom)
.heightToWidth(267/375)
vipRightsDetailView.layoutChain
@ -174,6 +172,7 @@ class VipRechargeView: UIView {
view.showsVerticalScrollIndicator = false
view.delegate = self
view.bounces = false
view.contentInsetAdjustmentBehavior = .never
return view
}()