parent
6163dd1ca8
commit
6eba31963b
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "logo108@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -20,7 +20,7 @@ extension DefaultsKeys {
|
|||
/// 重新登录(IM需要获取id和签名)
|
||||
var isReLogin: DefaultsKey<Bool> { .init("isReLogin", defaultValue: false) }
|
||||
/// 显示引导页
|
||||
var guideShowVersion: DefaultsKey<Int?> { .init("guideShowVersion") }
|
||||
var isShowGuide: DefaultsKey<Bool> { .init("isShowGuide", defaultValue: true) }
|
||||
}
|
||||
|
||||
/// 通知常量
|
||||
|
|
@ -35,6 +35,12 @@ extension Notification.Name {
|
|||
static let ShowMemberLocationNotification = Notification.Name("ShowMemberLocationNotification")
|
||||
/// 支付宝/微信支付结果回调
|
||||
static let RequestOrderPayStatusNotification = Notification.Name("RequestOrderPayStatusNotification")
|
||||
/// 暂停弹窗队列(第三方登录/注册跳转时)
|
||||
static let pausePopupQueue = Notification.Name("pausePopupQueue")
|
||||
/// 恢复弹窗队列(第三方登录取消/失败时)
|
||||
static let resumePopupQueue = Notification.Name("resumePopupQueue")
|
||||
/// 终止弹窗队列(登录成功后)
|
||||
static let invalidatePopupQueue = Notification.Name("invalidatePopupQueue")
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@ class BaseViewController: UIViewController {
|
|||
/// 导航栏标题
|
||||
var navTitle: String = "" {
|
||||
didSet {
|
||||
backButton.setTitle(" \(navTitle)", for: .normal)
|
||||
// navigationItem.title = navTitle
|
||||
navigationItem.title = navTitle
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,32 +101,13 @@ class BaseViewController: UIViewController {
|
|||
dismissItem.tintColor = .white
|
||||
navigationItem.leftBarButtonItem = dismissItem
|
||||
} else if let count = navigationController?.viewControllers.count, count > 1 {
|
||||
let leftBarBtn = UIBarButtonItem (customView: backButton)
|
||||
|
||||
//用于消除左边空隙,要不然按钮顶不到最前面
|
||||
let spacer = UIBarButtonItem (barButtonSystemItem: .fixedSpace, target: nil, action: nil)
|
||||
spacer.width = -10
|
||||
navigationItem.leftBarButtonItem = leftBarBtn
|
||||
// navigationItem.leftBarButtonItems = [spacer, leftBarBtn]
|
||||
// let leftBackItem = UIBarButtonItem(image: UIImage(named: "Common/back"),
|
||||
// style: .plain,
|
||||
// target: self, action: #selector(leftBackClicked(_:)))
|
||||
// leftBackItem.tintColor = .white
|
||||
// navigationItem.leftBarButtonItem = leftBackItem
|
||||
let leftBackItem = UIBarButtonItem(image: UIImage(named: "Common/back"),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(leftBackClicked(_:)))
|
||||
navigationItem.leftBarButtonItem = leftBackItem
|
||||
}
|
||||
}
|
||||
|
||||
lazy var backButton: UIButton = {
|
||||
let button = UIButton (type: .custom)
|
||||
button.frame = CGRectMake (0, 0, 200, 30)
|
||||
button.setImage(UIImage(named: "Common/back"), for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 18, weight: .medium)
|
||||
button.contentHorizontalAlignment = .left
|
||||
button.setTitleColor(ThemeManager.shared.color.titleColor, for: .normal)
|
||||
button.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
// MARK: - 验证码
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ final class MainTabBarController: UITabBarController {
|
|||
|
||||
viewControllers = navControllers
|
||||
tabBar.isHidden = true
|
||||
tabBar.removeFromSuperview()
|
||||
|
||||
for nav in navControllers {
|
||||
nav.delegate = self
|
||||
|
|
@ -71,7 +72,7 @@ final class MainTabBarController: UITabBarController {
|
|||
NSLayoutConstraint.activate([
|
||||
customTabBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
customTabBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
customTabBar.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -41),
|
||||
customTabBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
customTabBar.heightAnchor.constraint(equalToConstant: tabBarHeight)
|
||||
])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ struct SystemConfigModel: Mappable {
|
|||
var popupConfig: PopupWindowConfigModel?
|
||||
/// 弹窗时间配置
|
||||
var timeConfig: PopupWindowTimeModel?
|
||||
/// 微信分享配置
|
||||
var shareConfig: WxShareConfigModel?
|
||||
|
||||
/// 聊天界面的警告说明
|
||||
var chatWarning: String = ""
|
||||
|
|
@ -84,6 +86,7 @@ struct SystemConfigModel: Mappable {
|
|||
groupBannerList <- map["client.team.ad", nested: false]
|
||||
popupConfig <- map["client.popup.display", nested: false]
|
||||
timeConfig <- map["client.time.setting", nested: false]
|
||||
shareConfig <- map["client.weixin.share", nested: false]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,3 +135,28 @@ struct PopupWindowTimeModel: Mappable {
|
|||
pay_pop_type <- map["pay_pop_type"]
|
||||
}
|
||||
}
|
||||
|
||||
struct WxShareConfigModel: Mappable {
|
||||
/// 链接
|
||||
var link: String = ""
|
||||
|
||||
/// 标题
|
||||
var title: String = ""
|
||||
|
||||
/// 内容
|
||||
var content: String = ""
|
||||
|
||||
/// 图标
|
||||
var image: String = ""
|
||||
|
||||
init?(map: Map) {
|
||||
|
||||
}
|
||||
|
||||
mutating func mapping(map: Map) {
|
||||
link <- map["link"]
|
||||
title <- map["title"]
|
||||
content <- map["content"]
|
||||
image <- map["image"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ extension ApiManager {
|
|||
// MARK: - 处理Token过期
|
||||
private func handleTokenExpired(msg: String?) {
|
||||
MainAsync {
|
||||
NotificationCenter.default.post(name: .invalidatePopupQueue, object: nil)
|
||||
AppContextManager.shared.deleteAccount()
|
||||
GroupIMService.shared.logout()
|
||||
DLToast.showError(text: msg ?? "登录失效,请重新登录!") {
|
||||
|
|
|
|||
|
|
@ -28,8 +28,7 @@ class InviteJoinVC: BaseViewController {
|
|||
|
||||
// 分享
|
||||
rootView.shareAppBtn.rx.tap.subscribe(onNext: { _ in
|
||||
let url = "https://smartdrive.zuom8.cn/jisu/share/user/"
|
||||
SharePopView.show(shareURL: url)
|
||||
SharePopView.show()
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
guard let model = groupInModel else { return }
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ class HomeView: UIView {
|
|||
quickMessageView.isHidden = false
|
||||
interactionView.isHidden = false
|
||||
toolsView.isHidden = true
|
||||
searchLottieView.isHidden = true
|
||||
quickMessageView.frame.origin.y = kScreenHeight - 384 - 93
|
||||
interactionView.frame.origin.y = kScreenHeight - 384
|
||||
}
|
||||
|
|
@ -246,6 +247,7 @@ class HomeView: UIView {
|
|||
quickMessageView.isHidden = true
|
||||
interactionView.isHidden = true
|
||||
toolsView.isHidden = false
|
||||
searchLottieView.isHidden = false
|
||||
onDismissPanel?()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class HomeViewController: BaseViewController {
|
|||
private var subscribedMemberIds: [String] = []
|
||||
/// 底部成员面板是否显示
|
||||
private var isMemberPanelShown = false
|
||||
private var isSOSPlaying = false
|
||||
/// 记录每个成员最后一次收到位置的时间(MQTT userId → Date)
|
||||
private var lastUpdateTimes: [String: Date] = [:]
|
||||
/// 离线检测定时器
|
||||
|
|
@ -73,8 +74,6 @@ class HomeViewController: BaseViewController {
|
|||
setupHeading()
|
||||
reactiveAction()
|
||||
|
||||
requestUserConfig()
|
||||
|
||||
rootView.quickMessageView.tagListView.delegate = self
|
||||
// MQTT 位置上报
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
|
|
@ -189,9 +188,26 @@ class HomeViewController: BaseViewController {
|
|||
|
||||
// User Config刷新
|
||||
NotificationCenter.default.rx.notification(.RefreshUserConfigNotification, object: nil)
|
||||
.subscribe { [weak self] notification in
|
||||
self?.requestUserConfig()
|
||||
}.disposed(by: disposeBag)
|
||||
.startWith(Notification(name: .RefreshUserConfigNotification))
|
||||
.do(onNext: { [weak self] _ in
|
||||
self?.popupQueue = nil
|
||||
})
|
||||
.flatMapLatest { _ in
|
||||
SystemService.userConfig()
|
||||
}
|
||||
.subscribe(onNext: { [weak self] response in
|
||||
guard let self = self, let model = response.model else { return }
|
||||
Defaults[\.loginToken] = model.token
|
||||
AppContextManager.shared.systemConfig = model.config
|
||||
self.getUserIMToken()
|
||||
self.requestUserInfo { [weak self] in
|
||||
self?.requestGroupInfo()
|
||||
}
|
||||
self.requestNotice()
|
||||
self.popupQueue = nil
|
||||
self.popupQueue = PopupQueueManager()
|
||||
self.popupQueue?.start(from: self)
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
// 圈子刷新
|
||||
NotificationCenter.default.rx.notification(.RefreshGroupInfoNotification, object: nil)
|
||||
|
|
@ -248,10 +264,8 @@ class HomeViewController: BaseViewController {
|
|||
}
|
||||
|
||||
// 分享
|
||||
rootView.interactionView.onShare = { [weak self] in
|
||||
guard let self = self, let member = self.rootView.interactionView.currentMember else { return }
|
||||
let url = "https://smartdrive.zuom8.cn/jisu/share/user/\(member.id)"
|
||||
SharePopView.show(shareURL: url)
|
||||
rootView.interactionView.onShare = {
|
||||
SharePopView.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,25 +304,6 @@ 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 { [weak self] in
|
||||
self?.requestGroupInfo()
|
||||
}
|
||||
// 首页公告
|
||||
self.requestNotice()
|
||||
// 弹窗控制器
|
||||
self.popupQueue = PopupQueueManager()
|
||||
self.popupQueue?.start(from: self)
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
/// 获取用户IM Token
|
||||
func getUserIMToken() {
|
||||
DLToast.showLoading()
|
||||
|
|
@ -466,8 +461,8 @@ class HomeViewController: BaseViewController {
|
|||
let newMembers = others + [me]
|
||||
members = newMembers
|
||||
currentUserMember = me
|
||||
// 从 members 中过滤出在线成员用于地图标注
|
||||
let onlineMembers = newMembers.filter { $0.isOnline }
|
||||
// 从 members 中过滤出在线成员用于地图标注(当前用户坐标无效,由 GPS 回调单独添加)
|
||||
let onlineMembers = newMembers.filter { $0.isOnline && !$0.isCurrentUser }
|
||||
|
||||
guard let mapView = rootView.mapView else { return }
|
||||
|
||||
|
|
@ -477,10 +472,33 @@ class HomeViewController: BaseViewController {
|
|||
|
||||
let toRemove = existing.filter { !onlineIDs.contains($0.member.id) }
|
||||
mapView.removeAnnotations(toRemove)
|
||||
if toRemove.contains(where: { $0 === currentUserAnnotation }) {
|
||||
currentUserAnnotation = nil
|
||||
}
|
||||
|
||||
let toAdd = onlineMembers.filter { !existingIDs.contains($0.id) }
|
||||
let annotations = toAdd.map { MemberAnnotation(member: $0) }
|
||||
mapView.addAnnotations(annotations)
|
||||
|
||||
if currentUserAnnotation == nil,
|
||||
let me = currentUserMember,
|
||||
let loc = lastLocation,
|
||||
CLLocationCoordinate2DIsValid(loc.coordinate) {
|
||||
let updatedMe = CircleMember(
|
||||
id: "current",
|
||||
name: me.name,
|
||||
avatar: me.avatar,
|
||||
isOnline: true,
|
||||
isOwner: false,
|
||||
coordinate: loc.coordinate,
|
||||
address: me.address,
|
||||
heading: me.heading,
|
||||
lastUpdateTime: me.lastUpdateTime,
|
||||
battery: me.battery
|
||||
)
|
||||
currentUserMember = updatedMe
|
||||
mapView.addAnnotation(MemberAnnotation(member: updatedMe))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -639,15 +657,21 @@ extension HomeViewController {
|
|||
}
|
||||
}
|
||||
case "sos": // 求助
|
||||
self.requestGroupInfo(isDefaultGroup: false)
|
||||
guard let userId = msg.data?.user_id,
|
||||
userId != AppContextManager.shared.userId else { return }
|
||||
playSOSAlarm()
|
||||
rootView.sosPopView.isHidden = false
|
||||
rootView.sosPopView.play(toFrame: 30.0) { completed in
|
||||
guard completed else { return }
|
||||
self.rootView.sosPopView.stop()
|
||||
self.rootView.sosPopView.isHidden = true
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self, !self.isSOSPlaying else { return }
|
||||
self.isSOSPlaying = true
|
||||
self.playSOSAlarm()
|
||||
self.rootView.sosPopView.isHidden = false
|
||||
self.rootView.sosPopView.play { completed in
|
||||
self.rootView.sosPopView.stop()
|
||||
self.rootView.sosPopView.isHidden = true
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 15) { [weak self] in
|
||||
self?.rootView.sosPopView.stop()
|
||||
self?.rootView.sosPopView.isHidden = true
|
||||
}
|
||||
}
|
||||
case "join": // 圈子有成员加入
|
||||
guard let gk = msg.data?.group_key,
|
||||
|
|
@ -798,6 +822,22 @@ extension HomeViewController: MAMapViewDelegate {
|
|||
|
||||
if let ann = currentUserAnnotation {
|
||||
ann.coordinate = coordinate
|
||||
} else if let me = currentUserMember {
|
||||
let updatedMe = CircleMember(
|
||||
id: "current",
|
||||
name: me.name,
|
||||
avatar: me.avatar,
|
||||
isOnline: true,
|
||||
isOwner: false,
|
||||
coordinate: coordinate,
|
||||
address: me.address,
|
||||
heading: me.heading,
|
||||
lastUpdateTime: me.lastUpdateTime,
|
||||
battery: me.battery
|
||||
)
|
||||
currentUserMember = updatedMe
|
||||
let annotation = MemberAnnotation(member: updatedMe)
|
||||
mapView.addAnnotation(annotation)
|
||||
}
|
||||
|
||||
if !isMemberPanelShown {
|
||||
|
|
@ -835,13 +875,14 @@ extension HomeViewController {
|
|||
return
|
||||
}
|
||||
print("✅ SOS: playing alarm")
|
||||
player.numberOfLoops = 0
|
||||
player.numberOfLoops = -1
|
||||
player.volume = 1.0
|
||||
player.play()
|
||||
self.sosPlayer = player
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 15) { [weak self] in
|
||||
self?.sosPlayer?.stop()
|
||||
self?.sosPlayer = nil
|
||||
self?.isSOSPlaying = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,7 +108,10 @@ class InteractionView: UIView {
|
|||
let pages = (items.count + InteractionView.emojiPerPage - 1) / InteractionView.emojiPerPage
|
||||
self.emojiPageControl.numberOfPages = max(pages, 1)
|
||||
self.emojiPageControl.currentPage = 0
|
||||
self.emojiCollectionView.setContentOffset(.zero, animated: false)
|
||||
self.emojiCollectionView.layoutIfNeeded()
|
||||
DispatchQueue.main.async {
|
||||
self.emojiCollectionView.setContentOffset(.zero, animated: false)
|
||||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
|
@ -517,6 +520,11 @@ extension InteractionView: UICollectionViewDelegate {
|
|||
emojiPageControl.currentPage = page
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard collectionView == emojiCollectionView else { return }
|
||||
(cell as? EmojiPanelCell)?.playAnimation()
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
guard collectionView == emojiCollectionView else { return }
|
||||
(cell as? EmojiPanelCell)?.stopAnimation()
|
||||
|
|
@ -528,29 +536,46 @@ final class EmojiPanelCell: UICollectionViewCell {
|
|||
|
||||
private static var animationCache: [String: LottieAnimation] = [:]
|
||||
private static var loadingCompletions: [String: [(LottieAnimation?) -> Void]] = [:]
|
||||
private static let animationQueue = DispatchQueue(label: "com.quicklocation.emoji.animation", qos: .userInitiated)
|
||||
private static let cacheLock = NSLock()
|
||||
private static let animationQueue = DispatchQueue(label: "com.quicklocation.emoji.animation", qos: .userInitiated, attributes: .concurrent)
|
||||
|
||||
private static let emojiPathMap: [String: String] = {
|
||||
Bundle.main.paths(forResourcesOfType: "json", inDirectory: nil).reduce(into: [:]) { map, path in
|
||||
let name = (path as NSString).lastPathComponent.replacingOccurrences(of: ".json", with: "")
|
||||
map[name] = path
|
||||
}
|
||||
}()
|
||||
|
||||
static func loadAnimation(for name: String, completion: @escaping (LottieAnimation?) -> Void) {
|
||||
animationQueue.async {
|
||||
if let cached = animationCache[name] {
|
||||
Self.cacheLock.lock()
|
||||
if let cached = Self.animationCache[name] {
|
||||
Self.cacheLock.unlock()
|
||||
DispatchQueue.main.async { completion(cached) }
|
||||
return
|
||||
}
|
||||
|
||||
if loadingCompletions[name] != nil {
|
||||
loadingCompletions[name]?.append(completion)
|
||||
if Self.loadingCompletions[name] != nil {
|
||||
Self.loadingCompletions[name]?.append(completion)
|
||||
Self.cacheLock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
loadingCompletions[name] = [completion]
|
||||
Self.loadingCompletions[name] = [completion]
|
||||
Self.cacheLock.unlock()
|
||||
|
||||
let animation: LottieAnimation?
|
||||
if let path = Bundle.main.path(forResource: name, ofType: "json") {
|
||||
if let path = Self.emojiPathMap[name] {
|
||||
animation = LottieAnimation.filepath(path)
|
||||
} else {
|
||||
animation = nil
|
||||
}
|
||||
animationCache[name] = animation
|
||||
let completions = loadingCompletions.removeValue(forKey: name) ?? []
|
||||
|
||||
Self.cacheLock.lock()
|
||||
Self.animationCache[name] = animation
|
||||
let completions = Self.loadingCompletions.removeValue(forKey: name) ?? []
|
||||
Self.cacheLock.unlock()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completions.forEach { $0(animation) }
|
||||
}
|
||||
|
|
@ -558,7 +583,6 @@ final class EmojiPanelCell: UICollectionViewCell {
|
|||
}
|
||||
|
||||
private var animationName: String?
|
||||
private var shouldPlayAnimation = false
|
||||
|
||||
private let lottieView: LottieAnimationView = {
|
||||
let v = LottieAnimationView()
|
||||
|
|
@ -598,36 +622,28 @@ final class EmojiPanelCell: UICollectionViewCell {
|
|||
|
||||
func configure(name: String) {
|
||||
animationName = name
|
||||
shouldPlayAnimation = false
|
||||
lottieView.stop()
|
||||
lottieView.animation = nil
|
||||
lottieView.currentProgress = 0
|
||||
Self.loadAnimation(for: name) { [weak self] animation in
|
||||
guard let self = self, self.animationName == name else { return }
|
||||
self.lottieView.animation = animation
|
||||
self.lottieView.currentProgress = 0
|
||||
if self.shouldPlayAnimation {
|
||||
self.lottieView.play()
|
||||
}
|
||||
self.lottieView.play()
|
||||
}
|
||||
|
||||
lockView.isHidden = AppContextManager.shared.vip > 1
|
||||
}
|
||||
|
||||
func playAnimation() {
|
||||
shouldPlayAnimation = true
|
||||
lottieView.play()
|
||||
}
|
||||
|
||||
func stopAnimation() {
|
||||
shouldPlayAnimation = false
|
||||
lottieView.stop()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
animationName = nil
|
||||
shouldPlayAnimation = false
|
||||
lottieView.stop()
|
||||
lottieView.animation = nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,9 +34,7 @@ class LaunchViewController: BaseViewController {
|
|||
AppDelegate.shared.showMainViewController()
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
|
||||
getUserConfig()
|
||||
|
||||
navigateAfterDelay()
|
||||
}
|
||||
|
||||
|
|
@ -44,24 +42,16 @@ class LaunchViewController: BaseViewController {
|
|||
Observable.just(())
|
||||
.delay(.milliseconds(2500), scheduler: MainScheduler.instance)
|
||||
.subscribe(onNext: { [weak self] in
|
||||
self?.navigateToNext()
|
||||
if Defaults[\.isShowGuide] {
|
||||
self?.collectionView.isHidden = false
|
||||
self?.pageControl.isHidden = false
|
||||
self?.experienceBtn.isHidden = false
|
||||
} else {
|
||||
AppDelegate.shared.showMainViewController()
|
||||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
private func navigateToNext() {
|
||||
let hasShownGuide = UserDefaults.standard.bool(forKey: "hasShownGuide")
|
||||
if hasShownGuide {
|
||||
let loginVC = LoginViewController()
|
||||
UIView.transition(with: view.window!, duration: 0.4, options: .transitionCrossDissolve) {
|
||||
self.view.window?.rootViewController = UINavigationController(rootViewController: loginVC)
|
||||
}
|
||||
} else {
|
||||
collectionView.isHidden = false
|
||||
pageControl.isHidden = false
|
||||
experienceBtn.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户配置
|
||||
func getUserConfig() {
|
||||
|
|
@ -82,7 +72,6 @@ class LaunchViewController: BaseViewController {
|
|||
|
||||
/// 获取用户IM Token
|
||||
func getUserIMToken() {
|
||||
DLToast.showLoading()
|
||||
UserService.imToken().subscribe(onNext: { response in
|
||||
guard let data = response.data, let token = data["token"] as? String else { return }
|
||||
AppContextManager.shared.imToken = token
|
||||
|
|
|
|||
|
|
@ -68,17 +68,18 @@ class LoginView: UIView {
|
|||
addSubview(bgMaskImage)
|
||||
addSubview(backBtn)
|
||||
addSubview(guestLoginButton)
|
||||
addSubview(welcomeLabel)
|
||||
addSubview(phoneNumberLabel)
|
||||
addSubview(carrierLabel)
|
||||
addSubview(otherPhoneView)
|
||||
addSubview(inputContainerView)
|
||||
inputContainerView.addSubview(welcomeLabel)
|
||||
inputContainerView.addSubview(phoneNumberLabel)
|
||||
inputContainerView.addSubview(carrierLabel)
|
||||
inputContainerView.addSubview(otherPhoneView)
|
||||
otherPhoneView.addSubview(phoneInputView)
|
||||
otherPhoneView.addSubview(smsCodeInputView)
|
||||
addSubview(loginButton)
|
||||
inputContainerView.addSubview(loginButton)
|
||||
addSubview(appleLoginBtn)
|
||||
addSubview(wechatLoginBtn)
|
||||
addSubview(phoneLoginBtn)
|
||||
|
||||
|
||||
addSubview(agreementView)
|
||||
agreementView.addSubview(checkBox)
|
||||
agreementView.addSubview(agreementTV)
|
||||
|
|
@ -98,8 +99,14 @@ class LoginView: UIView {
|
|||
.width(85)
|
||||
.height(29)
|
||||
|
||||
inputContainerView.layoutChain
|
||||
.centerX()
|
||||
.centerY(self, offset: -17)
|
||||
.edgesHorzontal()
|
||||
.height(306)
|
||||
|
||||
welcomeLabel.layoutChain
|
||||
.top(202 - kNaviHeight)
|
||||
.top(0)
|
||||
.centerX()
|
||||
.edgesHorzontal()
|
||||
|
||||
|
|
@ -161,7 +168,7 @@ class LoginView: UIView {
|
|||
agreementView.layoutChain
|
||||
.leftToView(loginButton)
|
||||
.rightToView(loginButton)
|
||||
.bottom(55)
|
||||
.bottom(kSafeBottomMargin + 20)
|
||||
.height(30)
|
||||
|
||||
agreementLabel.layoutChain.edges()
|
||||
|
|
@ -178,7 +185,7 @@ class LoginView: UIView {
|
|||
.top()
|
||||
|
||||
phoneLoginBtn.layoutChain
|
||||
.bottomToTopOfView(agreementView, offset: -81)
|
||||
.bottomToTopOfView(agreementView, offset: -30)
|
||||
.centerX()
|
||||
.width(38)
|
||||
.heightToWidth(1)
|
||||
|
|
@ -258,6 +265,13 @@ class LoginView: UIView {
|
|||
return label
|
||||
}()
|
||||
|
||||
// 输入区域容器
|
||||
lazy var inputContainerView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
// 其他手机号登录
|
||||
lazy var otherPhoneView: UIView = {
|
||||
let view = UIView()
|
||||
|
|
@ -394,7 +408,6 @@ class LoginView: UIView {
|
|||
textView.backgroundColor = .clear
|
||||
textView.isEditable = false
|
||||
textView.isScrollEnabled = false
|
||||
textView.isSelectable = false
|
||||
textView.linkTextAttributes = [:]
|
||||
|
||||
return textView
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import UIKit
|
|||
import SnapKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import AuthenticationServices
|
||||
#if !targetEnvironment(simulator)
|
||||
import GeYanSdk
|
||||
#endif
|
||||
|
|
@ -39,14 +40,14 @@ class LoginViewController: BaseViewController {
|
|||
bindViewModel()
|
||||
reactiveAction()
|
||||
}
|
||||
|
||||
|
||||
private func reactiveAction() {
|
||||
rootView.smsCodeBtn.rx.controlEvent(.touchUpInside).subscribe { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.requestSmsCodeAPI()
|
||||
}.disposed(by: rootView.disposeBag)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Bindings
|
||||
private func bindViewModel() {
|
||||
rootView.checkBox.rx.tap
|
||||
|
|
@ -100,17 +101,40 @@ class LoginViewController: BaseViewController {
|
|||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
viewModel.onAppleLoginRequest = { [weak self] controller in
|
||||
guard let self = self else { return }
|
||||
controller.delegate = self
|
||||
controller.presentationContextProvider = self
|
||||
controller.performRequests()
|
||||
}
|
||||
|
||||
rootView.phoneLoginBtn.rx.tap
|
||||
.subscribe(onNext: { [weak self] in
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
if GeYanSdk.getCurrentCarrierCount() == 0 {
|
||||
DLToast.show(text: "请插入SIM卡后重试")
|
||||
return
|
||||
}
|
||||
GeYanSdk.preGetToken { preDic in
|
||||
guard let dict = preDic as? [String: Any],
|
||||
dict["code"] as? Int == 30000,
|
||||
let model = GyContentModel.current(),
|
||||
!model.pn.isEmpty else {
|
||||
DLToast.showError(text: "获取手机号失败,请使用验证码登录")
|
||||
return
|
||||
}
|
||||
let oneTapView = OneTapLoginView(frame: UIScreen.main.bounds)
|
||||
self?.viewModel.performOneTapLogin(from: self!, contentView: oneTapView)
|
||||
}
|
||||
#endif
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
viewModel.loginSuccess
|
||||
.subscribe(onNext: { [weak self] in
|
||||
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
// viewModel.loginSuccess
|
||||
// .subscribe(onNext: { [weak self] in
|
||||
//
|
||||
// })
|
||||
// .disposed(by: disposeBag)
|
||||
}
|
||||
|
||||
// MARK: - API 验证码
|
||||
|
|
@ -150,3 +174,40 @@ extension LoginViewController: UITextViewDelegate {
|
|||
|
||||
private func openURL(_ url: String) {}
|
||||
}
|
||||
|
||||
// MARK: - Apple Sign In
|
||||
extension LoginViewController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
|
||||
|
||||
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
||||
view.window ?? UIWindow()
|
||||
}
|
||||
|
||||
func authorizationController(controller: ASAuthorizationController,
|
||||
didCompleteWithAuthorization authorization: ASAuthorization) {
|
||||
guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else { return }
|
||||
|
||||
let userId = credential.user
|
||||
let userName: String = {
|
||||
if let name = credential.fullName {
|
||||
let parts = [name.givenName, name.familyName].compactMap { $0 }
|
||||
if !parts.isEmpty { return parts.joined(separator: " ") }
|
||||
}
|
||||
return ""
|
||||
}()
|
||||
let identityToken = credential.identityToken.flatMap { String(data: $0, encoding: .utf8) } ?? ""
|
||||
let authCode = credential.authorizationCode.flatMap { String(data: $0, encoding: .utf8) } ?? ""
|
||||
|
||||
viewModel.loginAction(type: "apple", data: [
|
||||
"user_id": userId,
|
||||
"user_name": userName,
|
||||
"identify_code": authCode,
|
||||
"identify_token": identityToken
|
||||
])
|
||||
}
|
||||
|
||||
func authorizationController(controller: ASAuthorizationController,
|
||||
didCompleteWithError error: Error) {
|
||||
NotificationCenter.default.post(name: .resumePopupQueue, object: nil)
|
||||
DLToast.showError(text: "Apple 登录失败")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,43 @@
|
|||
import Foundation
|
||||
import RxSwift
|
||||
import SwiftyUserDefaults
|
||||
import AuthenticationServices
|
||||
#if !targetEnvironment(simulator)
|
||||
import GeYanSdk
|
||||
#endif
|
||||
|
||||
enum LoginSessionHandler {
|
||||
private static let disposeBag = DisposeBag()
|
||||
|
||||
static func login(type: String, data: [String: Any]) {
|
||||
DLToast.showLoading()
|
||||
UserService.login(type: type, data: data)
|
||||
.delaySubscription(.seconds(1), scheduler: MainScheduler.instance)
|
||||
.retry(1)
|
||||
.subscribe(onNext: { response in
|
||||
GroupIMService.shared.logout()
|
||||
DLToast.dismiss()
|
||||
guard let model = response.model else { return }
|
||||
NotificationCenter.default.post(name: .invalidatePopupQueue, object: nil)
|
||||
Defaults[\.loginToken] = model.token
|
||||
DLToast.showSuccess(text: "登录成功") {
|
||||
if let userId = model.uid {
|
||||
MQTTService.shared.updateClientID("smartdrive_\(userId)")
|
||||
}
|
||||
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
|
||||
if let nav = AppRouter.shared.navigationController, nav.viewControllers.count > 1 {
|
||||
AppRouter.shared.popOrDismiss()
|
||||
} else {
|
||||
AppDelegate.shared.showMainViewController()
|
||||
}
|
||||
}
|
||||
}, onError: { error in
|
||||
DLToast.dismiss()
|
||||
DLToast.showError(text: "登录失败,请重试")
|
||||
}).disposed(by: disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
final class LoginViewModel: BaseViewModel {
|
||||
|
||||
/// 验证码接口返回值
|
||||
|
|
@ -18,47 +51,78 @@ final class LoginViewModel: BaseViewModel {
|
|||
let oneClickLoginResult = PublishSubject<Bool>()
|
||||
let loginSuccess = PublishSubject<Void>()
|
||||
|
||||
var gyAuthVM: GyAuthViewModel {
|
||||
var onAppleLoginRequest: ((ASAuthorizationController) -> Void)?
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
lazy var gyAuthVM: GyAuthViewModel = {
|
||||
let viewModel = GyAuthViewModel()
|
||||
viewModel.statusBarStyle = .default
|
||||
viewModel.userInterfaceStyle = 0
|
||||
viewModel.pullAuthVCStyle = .push
|
||||
viewModel.viewLifeCycleBlock = { (str, bool) in
|
||||
|
||||
}
|
||||
viewModel.pullAuthVCStyle = .modal
|
||||
viewModel.clickAuthButtonBlock = {
|
||||
return true
|
||||
}
|
||||
return viewModel
|
||||
}
|
||||
}()
|
||||
#endif
|
||||
|
||||
/// 登录
|
||||
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 }
|
||||
Defaults[\.loginToken] = model.token
|
||||
DLToast.showSuccess(text: "登录成功") {
|
||||
if let userId = model.uid {
|
||||
MQTTService.shared.updateClientID("smartdrive_\(userId)")
|
||||
}
|
||||
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
|
||||
AppRouter.shared.popOrDismiss()
|
||||
}
|
||||
}.disposed(by: disposeBag)
|
||||
LoginSessionHandler.login(type: type, data: data)
|
||||
}
|
||||
|
||||
func performGuestLogin() {
|
||||
loginSuccess.onNext(())
|
||||
}
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
func performOneTapLogin(from vc: UIViewController, contentView: OneTapLoginView) {
|
||||
NotificationCenter.default.post(name: .pausePopupQueue, object: nil)
|
||||
let subViews: [UIView] = [
|
||||
contentView.phoneNumberLabel,
|
||||
contentView.carrierLabel,
|
||||
contentView.loginButton,
|
||||
contentView.checkBox,
|
||||
contentView.agreementTV
|
||||
]
|
||||
GeYanSdk.oneTapLogin(vc, viewModel: gyAuthVM, contentView: contentView,
|
||||
subViewList: subViews) { [weak self] result in
|
||||
guard let dict = result as? [String: Any],
|
||||
dict["code"] as? Int == 30000,
|
||||
let token = dict["token"] as? String else { return }
|
||||
|
||||
let opType: String = {
|
||||
switch dict["operatorType"] as? Int {
|
||||
case 1: return "CM"
|
||||
case 2: return "CU"
|
||||
case 3: return "CT"
|
||||
default: return ""
|
||||
}
|
||||
}()
|
||||
|
||||
self?.loginAction(type: "onekey", data: [
|
||||
"auth_code": "",
|
||||
"access_code": token,
|
||||
"operator_type": opType
|
||||
])
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
func performWechatLogin() {
|
||||
// TODO: Integrate WeChat SDK
|
||||
NotificationCenter.default.post(name: .pausePopupQueue, object: nil)
|
||||
let req = SendAuthReq()
|
||||
req.scope = "snsapi_userinfo"
|
||||
req.state = "login"
|
||||
WXApi.send(req)
|
||||
}
|
||||
|
||||
func performAppleLogin() {
|
||||
// TODO: Integrate Sign in with Apple
|
||||
NotificationCenter.default.post(name: .pausePopupQueue, object: nil)
|
||||
let provider = ASAuthorizationAppleIDProvider()
|
||||
let request = provider.createRequest()
|
||||
request.requestedScopes = [.fullName, .email]
|
||||
let controller = ASAuthorizationController(authorizationRequests: [request])
|
||||
onAppleLoginRequest?(controller)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ class OneTapLoginView: UIView {
|
|||
|
||||
private func setupRx() {
|
||||
backBtn.rx.tap.subscribe(onNext: { _ in
|
||||
AppRouter.shared.popOrDismiss()
|
||||
#if !targetEnvironment(simulator)
|
||||
GeYanSdk.closeAuthVC(true, completion: nil)
|
||||
#endif
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
checkBox.rx.tap.subscribe(onNext: { [weak self] in
|
||||
|
|
@ -55,11 +57,12 @@ class OneTapLoginView: UIView {
|
|||
private func setupUI() {
|
||||
addSubview(bgMaskImage)
|
||||
addSubview(backBtn)
|
||||
addSubview(welcomeLabel)
|
||||
addSubview(phoneNumberLabel)
|
||||
addSubview(carrierLabel)
|
||||
addSubview(loginButton)
|
||||
|
||||
addSubview(inputContainerView)
|
||||
inputContainerView.addSubview(welcomeLabel)
|
||||
inputContainerView.addSubview(phoneNumberLabel)
|
||||
inputContainerView.addSubview(carrierLabel)
|
||||
inputContainerView.addSubview(loginButton)
|
||||
|
||||
addSubview(agreementView)
|
||||
agreementView.addSubview(checkBox)
|
||||
agreementView.addSubview(agreementTV)
|
||||
|
|
@ -73,8 +76,14 @@ class OneTapLoginView: UIView {
|
|||
.width(24)
|
||||
.height(24)
|
||||
|
||||
inputContainerView.layoutChain
|
||||
.centerX()
|
||||
.centerY()
|
||||
.edgesHorzontal()
|
||||
.height(301)
|
||||
|
||||
welcomeLabel.layoutChain
|
||||
.top(202 - kNaviHeight)
|
||||
.top(0)
|
||||
.centerX()
|
||||
.edgesHorzontal()
|
||||
|
||||
|
|
@ -130,6 +139,12 @@ class OneTapLoginView: UIView {
|
|||
}
|
||||
|
||||
// MARK: - UI Components
|
||||
lazy var inputContainerView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
lazy var bgMaskImage: UIImageView = {
|
||||
let iv = UIImageView()
|
||||
iv.image = UIImage(named: "Login/bg")
|
||||
|
|
@ -207,7 +222,6 @@ class OneTapLoginView: UIView {
|
|||
textView.backgroundColor = .clear
|
||||
textView.isEditable = false
|
||||
textView.isScrollEnabled = false
|
||||
textView.isSelectable = false
|
||||
textView.linkTextAttributes = [:]
|
||||
|
||||
return textView
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class AccountVC: BaseViewController {
|
|||
DLToast.showLoading()
|
||||
UserService.logout().subscribe(onNext: { response in
|
||||
DLToast.dismiss()
|
||||
NotificationCenter.default.post(name: .invalidatePopupQueue, object: nil)
|
||||
AppContextManager.shared.deleteAccount()
|
||||
GroupIMService.shared.logout()
|
||||
AppDelegate.shared.showMainViewController()
|
||||
|
|
|
|||
|
|
@ -188,6 +188,10 @@ final class MineView: UIView {
|
|||
// MARK: - Callbacks
|
||||
var onMenuTap: ((Int) -> Void)?
|
||||
|
||||
// MARK: - Dynamic Height
|
||||
private var settingsCardHeightConstraint: NSLayoutConstraint?
|
||||
private var contentSizeObservation: NSKeyValueObservation?
|
||||
|
||||
// MARK: - Init
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
|
@ -289,14 +293,42 @@ final class MineView: UIView {
|
|||
.topToBottomOfView(profileBgImageView)
|
||||
.left().right().bottom()
|
||||
|
||||
// Settings Card (fixed height, not stretched)
|
||||
// Settings Card (dynamic height, scrolls when exceeds area above tabbar)
|
||||
settingsCardView.layoutChain
|
||||
.topToBottomOfView(vipCardView, offset: 20)
|
||||
.left(16).right(16)
|
||||
.height(312)
|
||||
|
||||
settingsCardHeightConstraint = settingsCardView.heightAnchor.constraint(equalToConstant: 312)
|
||||
settingsCardHeightConstraint?.isActive = true
|
||||
|
||||
settingsTableView.layoutChain
|
||||
.edges()
|
||||
|
||||
contentSizeObservation = settingsTableView.observe(\.contentSize, options: [.new, .initial]) { [weak self] _, _ in
|
||||
self?.refreshTableViewHeight()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
refreshTableViewHeight()
|
||||
}
|
||||
|
||||
func refreshTableViewHeight() {
|
||||
let contentHeight = settingsTableView.contentSize.height
|
||||
guard contentHeight > 0 else { return }
|
||||
|
||||
let tabBarHeight: CGFloat = 56 + kSafeBottomMargin
|
||||
let maxHeight = bounds.height - tabBarHeight - settingsCardView.frame.minY
|
||||
|
||||
if contentHeight <= maxHeight {
|
||||
settingsCardHeightConstraint?.constant = contentHeight
|
||||
settingsTableView.isScrollEnabled = false
|
||||
} else {
|
||||
settingsCardHeightConstraint?.constant = maxHeight
|
||||
settingsTableView.isScrollEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,28 @@ class PopupQueueManager {
|
|||
|
||||
private weak var sourceVC: UIViewController?
|
||||
private var currentStep = 0
|
||||
private var stepGeneration = 0
|
||||
private var countdownTimer: Timer?
|
||||
private var isPaused = false
|
||||
|
||||
init() {
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(handlePause),
|
||||
name: .pausePopupQueue,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(handleResume),
|
||||
name: .resumePopupQueue,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(handleInvalidate),
|
||||
name: .invalidatePopupQueue,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
func start(from vc: UIViewController) {
|
||||
sourceVC = vc
|
||||
|
|
@ -19,6 +40,25 @@ class PopupQueueManager {
|
|||
executeNextStep()
|
||||
}
|
||||
|
||||
@objc private func handlePause() {
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = nil
|
||||
isPaused = true
|
||||
}
|
||||
|
||||
@objc private func handleResume() {
|
||||
guard isPaused else { return }
|
||||
isPaused = false
|
||||
executeNextStep()
|
||||
}
|
||||
|
||||
@objc private func handleInvalidate() {
|
||||
countdownTimer?.invalidate()
|
||||
countdownTimer = nil
|
||||
isPaused = true
|
||||
currentStep = 3
|
||||
}
|
||||
|
||||
private func executeNextStep() {
|
||||
switch currentStep {
|
||||
case 0:
|
||||
|
|
@ -45,12 +85,14 @@ class PopupQueueManager {
|
|||
skipToNext()
|
||||
return
|
||||
}
|
||||
let gen = stepGeneration
|
||||
startCountdown(seconds: timeConfig.pay_pop_time) { [weak self] in
|
||||
guard let self = self, self.currentStep == 0, self.stepGeneration == gen else { return }
|
||||
let vc = PromotionalActivitiesVC()
|
||||
vc.onDismiss = { [weak self] in
|
||||
self?.skipToNext()
|
||||
}
|
||||
self?.push(vc)
|
||||
self.push(vc)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,9 +108,11 @@ class PopupQueueManager {
|
|||
skipToNext()
|
||||
return
|
||||
}
|
||||
startCountdown(seconds: timeConfig.search_phone) { [weak self] in
|
||||
let gen = stepGeneration
|
||||
startCountdown(seconds: timeConfig.search_phone) {
|
||||
guard self.currentStep == 1, self.stepGeneration == gen else { return }
|
||||
PopupWindow.show(popupType: .search) {
|
||||
self?.skipToNext()
|
||||
self.skipToNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -82,13 +126,17 @@ class PopupQueueManager {
|
|||
}
|
||||
|
||||
if AppContextManager.shared.isGuest {
|
||||
startCountdown(seconds: timeConfig.signup_tip) { [weak self] in
|
||||
let gen = stepGeneration
|
||||
startCountdown(seconds: timeConfig.signup_tip) {
|
||||
guard self.currentStep == 2, self.stepGeneration == gen else { return }
|
||||
PopupWindow.show(popupType: .register) {
|
||||
// done
|
||||
}
|
||||
}
|
||||
} else if AppContextManager.shared.vip == 1 {
|
||||
startCountdown(seconds: timeConfig.buy_vip_tip) { [weak self] in
|
||||
let gen = stepGeneration
|
||||
startCountdown(seconds: timeConfig.buy_vip_tip) {
|
||||
guard self.currentStep == 2, self.stepGeneration == gen else { return }
|
||||
PopupWindow.show(popupType: .vip) {
|
||||
// done
|
||||
}
|
||||
|
|
@ -99,6 +147,7 @@ class PopupQueueManager {
|
|||
// MARK: - Helpers
|
||||
|
||||
private func skipToNext() {
|
||||
stepGeneration += 1
|
||||
currentStep += 1
|
||||
executeNextStep()
|
||||
}
|
||||
|
|
@ -122,7 +171,11 @@ class PopupQueueManager {
|
|||
|
||||
private func push(_ vc: UIViewController) {
|
||||
let topVC = topViewController()
|
||||
topVC?.navigationController?.pushViewController(vc, animated: true)
|
||||
guard let nav = topVC?.navigationController else { return }
|
||||
if nav.viewControllers.contains(where: { type(of: $0) == type(of: vc) }) {
|
||||
return
|
||||
}
|
||||
nav.pushViewController(vc, animated: true)
|
||||
}
|
||||
|
||||
private func topViewController() -> UIViewController? {
|
||||
|
|
|
|||
|
|
@ -151,8 +151,12 @@ class PopupWindow: UIView {
|
|||
|
||||
registerBtn.rx.tap
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
NotificationCenter.default.post(name: .pausePopupQueue, object: nil)
|
||||
self?.activePopupView == 2 ? self?.slideOutRegister2Popup() : self?.slideOutRegisterPopup()
|
||||
AppRouter.push(Route.login)
|
||||
let topVC = UIViewController.currentViewController()
|
||||
if !(topVC is LoginViewController) {
|
||||
AppRouter.push(Route.login)
|
||||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
|
|
@ -164,8 +168,12 @@ class PopupWindow: UIView {
|
|||
|
||||
register2Btn.rx.tap
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
NotificationCenter.default.post(name: .pausePopupQueue, object: nil)
|
||||
self?.slideOutRegister2Popup()
|
||||
AppRouter.push(Route.login)
|
||||
let topVC = UIViewController.currentViewController()
|
||||
if !(topVC is LoginViewController) {
|
||||
AppRouter.push(Route.login)
|
||||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
|
|
@ -178,6 +186,10 @@ class PopupWindow: UIView {
|
|||
vip1UnlockBtn.rx.tap
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.slideOutUnlockVip1Popup()
|
||||
let topVC = UIViewController.currentViewController()
|
||||
if !(topVC is VipRechargeVC) {
|
||||
AppRouter.push(Route.vipRecharge)
|
||||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
|
|
@ -190,6 +202,10 @@ class PopupWindow: UIView {
|
|||
unlockVip2Btn.rx.tap
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.slideOutUnlockVip2Popup()
|
||||
let topVC = UIViewController.currentViewController()
|
||||
if !(topVC is VipRechargeVC) {
|
||||
AppRouter.push(Route.vipRecharge)
|
||||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
|
|
@ -202,6 +218,10 @@ class PopupWindow: UIView {
|
|||
unlockVip3Btn.rx.tap
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.slideOutUnlockVip3Popup()
|
||||
let topVC = UIViewController.currentViewController()
|
||||
if !(topVC is VipRechargeVC) {
|
||||
AppRouter.push(Route.vipRecharge)
|
||||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
|
|
@ -214,6 +234,10 @@ class PopupWindow: UIView {
|
|||
unlockVip4Btn.rx.tap
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.slideOutUnlockVip4Popup()
|
||||
let topVC = UIViewController.currentViewController()
|
||||
if !(topVC is VipRechargeVC) {
|
||||
AppRouter.push(Route.vipRecharge)
|
||||
}
|
||||
})
|
||||
.disposed(by: disposeBag)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class PromotionalActivitiesVC: BaseViewController {
|
|||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
fd_interactivePopDisabled = true
|
||||
setupTableView()
|
||||
requestRechargeInfo()
|
||||
|
||||
|
|
@ -103,6 +103,11 @@ class PromotionalActivitiesVC: BaseViewController {
|
|||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
navigationController?.setNavigationBarHidden(true, animated: false)
|
||||
}
|
||||
|
||||
// MARK: - API
|
||||
private func requestRechargeInfo() {
|
||||
DLToast.showLoading()
|
||||
|
|
@ -126,29 +131,29 @@ class PromotionalActivitiesVC: BaseViewController {
|
|||
return
|
||||
}
|
||||
|
||||
if timeConfig.pay_pop_type == 3 {
|
||||
IAPManager.shared.purchase(goodsId: selectedGoodsId) { [weak self] success, msg in
|
||||
guard let self = self else { return }
|
||||
if success {
|
||||
self.showPayResultPop(showCloseBtn: false,
|
||||
status: true,
|
||||
title: "支付成功",
|
||||
message: "恭喜您!成功开通!",
|
||||
confirmText: "确定", confirmBlock: {
|
||||
AppRouter.shared.popOrDismiss()
|
||||
}, cancelBlock: { })
|
||||
} else {
|
||||
self.showPayResultPop(status: false,
|
||||
title: "支付失败",
|
||||
message: msg ?? "很抱歉,请您重新支付!",
|
||||
confirmText: "重新支付", confirmBlock: { },
|
||||
cancelText: "取消", cancelBlock: {
|
||||
AppRouter.shared.popOrDismiss()
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
// if timeConfig.pay_pop_type == 3 {
|
||||
// IAPManager.shared.purchase(goodsId: selectedGoodsId) { [weak self] success, msg in
|
||||
// guard let self = self else { return }
|
||||
// if success {
|
||||
// self.showPayResultPop(showCloseBtn: false,
|
||||
// status: true,
|
||||
// title: "支付成功",
|
||||
// message: "恭喜您!成功开通!",
|
||||
// confirmText: "确定", confirmBlock: {
|
||||
// AppRouter.shared.popOrDismiss()
|
||||
// }, cancelBlock: { })
|
||||
// } else {
|
||||
// self.showPayResultPop(status: false,
|
||||
// title: "支付失败",
|
||||
// message: msg ?? "很抱歉,请您重新支付!",
|
||||
// confirmText: "重新支付", confirmBlock: { },
|
||||
// cancelText: "取消", cancelBlock: {
|
||||
// AppRouter.shared.popOrDismiss()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
|
||||
let payType = timeConfig.pay_pop_type == 1 ? "alipay" : "weixin"
|
||||
dl.showLoading()
|
||||
|
|
|
|||
|
|
@ -533,7 +533,7 @@ class PromotionalRecommendCell: UITableViewCell {
|
|||
|
||||
tipsTitleLab.layoutChain
|
||||
.centerX()
|
||||
.top(5)
|
||||
.top(7)
|
||||
|
||||
priceLab.layoutChain
|
||||
.leftToRightOfView(unitLab, offset: 0)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,16 @@ class ScheduleVC: BaseViewController {
|
|||
self.rootView.tableView.refresh(status: status, isEmpty: isEmpty)
|
||||
}).disposed(by: disposeBag)
|
||||
|
||||
viewModel.output.sectionedItems
|
||||
.map { !($0.first?.items.isEmpty ?? true) }
|
||||
.bind(to: rootView.emptyVisitorsLabel.rx.isHidden)
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
viewModel.output.scheduleSectionedItems
|
||||
.map { !($0.first?.items.isEmpty ?? true) }
|
||||
.bind(to: rootView.emptyScheduleLabel.rx.isHidden)
|
||||
.disposed(by: disposeBag)
|
||||
|
||||
rootView.collectionView.rx.modelSelected(ViewedModel.self)
|
||||
.subscribe(viewModel.viewedCellAction.inputs)
|
||||
.disposed(by: disposeBag)
|
||||
|
|
|
|||
|
|
@ -128,6 +128,12 @@ class ScheduleView: UIView {
|
|||
.edgesHorzontal()
|
||||
.height(80)
|
||||
.bottom(15)
|
||||
|
||||
// 暂无访客
|
||||
view.addSubview(emptyVisitorsLabel)
|
||||
emptyVisitorsLabel.layoutChain
|
||||
.centerY(collectionView)
|
||||
.centerX()
|
||||
|
||||
return view
|
||||
}()
|
||||
|
|
@ -145,6 +151,16 @@ class ScheduleView: UIView {
|
|||
return cv
|
||||
}()
|
||||
|
||||
lazy var emptyVisitorsLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "暂无访客"
|
||||
label.font = .systemFont(ofSize: 14)
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
label.textAlignment = .center
|
||||
label.isHidden = true
|
||||
return label
|
||||
}()
|
||||
|
||||
/// 行程路线
|
||||
lazy var travelRouteView: UIView = {
|
||||
let view = UIView()
|
||||
|
|
@ -213,9 +229,20 @@ class ScheduleView: UIView {
|
|||
tableView.showsVerticalScrollIndicator = false
|
||||
tableView.register(ScheduleListPopCell.self)
|
||||
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 97 + kSafeBottomMargin, right: 0)
|
||||
tableView.backgroundView = emptyScheduleLabel
|
||||
return tableView
|
||||
}()
|
||||
|
||||
lazy var emptyScheduleLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.text = "暂无行程"
|
||||
label.font = .systemFont(ofSize: 14)
|
||||
label.textColor = UIColor(hexStr: "#999999")
|
||||
label.textAlignment = .center
|
||||
label.isHidden = true
|
||||
return label
|
||||
}()
|
||||
|
||||
lazy var createBtn: UIButton = {
|
||||
let btn = UIButton()
|
||||
btn.setImage(UIImage(named: "Schedule/create"), for: .normal)
|
||||
|
|
|
|||
|
|
@ -6,22 +6,22 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import Kingfisher
|
||||
|
||||
class SharePopView: UIView {
|
||||
|
||||
private static let shared = SharePopView(frame: CGRect(origin: .zero, size: kScreenSize))
|
||||
private var shareURL: String = ""
|
||||
|
||||
|
||||
private let contentHeight: CGFloat = 191 + kSafeBottomMargin
|
||||
|
||||
static func show(shareURL: String) {
|
||||
static func show() {
|
||||
guard let superView = kKeyWindow else { return }
|
||||
|
||||
if SharePopView.shared.superview != nil {
|
||||
SharePopView.shared.dismiss(animated: false)
|
||||
}
|
||||
|
||||
SharePopView.shared.shareURL = shareURL
|
||||
SharePopView.shared.bgView.alpha = 0
|
||||
superView.addSubview(SharePopView.shared)
|
||||
superView.bringSubviewToFront(SharePopView.shared)
|
||||
|
|
@ -65,19 +65,52 @@ class SharePopView: UIView {
|
|||
}
|
||||
|
||||
private func shareToWeChat(scene: Int32) {
|
||||
let message = WXMediaMessage()
|
||||
message.title = kAppName
|
||||
let webpage = WXWebpageObject()
|
||||
webpage.webpageUrl = shareURL
|
||||
message.mediaObject = webpage
|
||||
guard let config = AppContextManager.shared.systemConfig,
|
||||
let shareConfig = config.shareConfig else { return }
|
||||
|
||||
let req = SendMessageToWXReq()
|
||||
req.bText = false
|
||||
req.message = message
|
||||
req.scene = scene
|
||||
WXApi.send(req)
|
||||
DLToast.showLoading()
|
||||
|
||||
dismiss()
|
||||
let fallbackImage = UIImage(named: "Common/logo108")
|
||||
let imageURL: URL? = {
|
||||
if shareConfig.image.isEmpty { return nil }
|
||||
return URL(string: shareConfig.image)
|
||||
}()
|
||||
|
||||
let sendMessage: (UIImage?) -> Void = { [weak self] thumbImage in
|
||||
guard let self = self else { return }
|
||||
DLToast.dismiss()
|
||||
let message = WXMediaMessage()
|
||||
message.title = shareConfig.title
|
||||
message.description = shareConfig.content
|
||||
if let thumb = thumbImage {
|
||||
message.setThumbImage(thumb)
|
||||
}
|
||||
|
||||
let webpage = WXWebpageObject()
|
||||
webpage.webpageUrl = shareConfig.link
|
||||
message.mediaObject = webpage
|
||||
|
||||
let req = SendMessageToWXReq()
|
||||
req.bText = false
|
||||
req.message = message
|
||||
req.scene = scene
|
||||
WXApi.send(req)
|
||||
|
||||
self.dismiss()
|
||||
}
|
||||
|
||||
if let url = imageURL {
|
||||
KingfisherManager.shared.retrieveImage(with: url) { result in
|
||||
switch result {
|
||||
case .success(let value):
|
||||
sendMessage(value.image)
|
||||
case .failure:
|
||||
sendMessage(fallbackImage)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sendMessage(fallbackImage)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ class VipRechargeVC: BaseViewController {
|
|||
requestRechargeInfo()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
navigationController?.setNavigationBarHidden(true, animated: false)
|
||||
}
|
||||
|
||||
private func bindViewModel() {
|
||||
viewModel.output.sectionedItems
|
||||
.bind(to: rootView.expenseCollectionView.rx.items(dataSource: dataSource))
|
||||
|
|
@ -102,29 +107,29 @@ class VipRechargeVC: BaseViewController {
|
|||
let types = viewModel.payType.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
let payType = types[rootView.selectedPayTypeTag]
|
||||
|
||||
if payType == "apple" {
|
||||
IAPManager.shared.purchase(goodsId: viewModel.goodsId) { [weak self] success, msg in
|
||||
guard let self = self else { return }
|
||||
if success {
|
||||
self.showPayResultPop(showCloseBtn: false,
|
||||
status: true,
|
||||
title: "支付成功",
|
||||
message: "恭喜您!成功开通\(self.viewModel.goodsName)",
|
||||
confirmText: "确定", confirmBlock: {
|
||||
AppRouter.shared.popOrDismiss()
|
||||
}, cancelBlock: { })
|
||||
} else {
|
||||
self.showPayResultPop(status: false,
|
||||
title: "支付失败",
|
||||
message: msg ?? "很抱歉,请您重新支付!",
|
||||
confirmText: "重新支付", confirmBlock: { },
|
||||
cancelText: "取消", cancelBlock: {
|
||||
AppRouter.shared.popOrDismiss()
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
// if payType == "apple" {
|
||||
// IAPManager.shared.purchase(goodsId: viewModel.goodsId) { [weak self] success, msg in
|
||||
// guard let self = self else { return }
|
||||
// if success {
|
||||
// self.showPayResultPop(showCloseBtn: false,
|
||||
// status: true,
|
||||
// title: "支付成功",
|
||||
// message: "恭喜您!成功开通\(self.viewModel.goodsName)",
|
||||
// confirmText: "确定", confirmBlock: {
|
||||
// AppRouter.shared.popOrDismiss()
|
||||
// }, cancelBlock: { })
|
||||
// } else {
|
||||
// self.showPayResultPop(status: false,
|
||||
// title: "支付失败",
|
||||
// message: msg ?? "很抱歉,请您重新支付!",
|
||||
// confirmText: "重新支付", confirmBlock: { },
|
||||
// cancelText: "取消", cancelBlock: {
|
||||
// AppRouter.shared.popOrDismiss()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
|
||||
DLToast.showLoading()
|
||||
OrderService.orderPayParams(goodsId: viewModel.goodsId,
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class VipRechargeView: UIView {
|
|||
.heightToWidth(267/375)
|
||||
|
||||
vipRightsDetailView.layoutChain
|
||||
.top(36)
|
||||
.top(36 + kStatusBarHeight)
|
||||
.right()
|
||||
.height(24)
|
||||
|
||||
|
|
@ -562,7 +562,7 @@ class VipRechargeView: UIView {
|
|||
let payTypeMap: [(String, String)] = types.compactMap {
|
||||
if $0 == "weixin" { return ("wechat", "微信支付") }
|
||||
if $0 == "alipay" { return ("alipay", "支付宝支付") }
|
||||
if $0 == "apple" { return ("apple", "苹果支付") }
|
||||
// if $0 == "apple" { return ("apple", "苹果支付") }
|
||||
return nil
|
||||
}
|
||||
guard !payTypeMap.isEmpty else { return }
|
||||
|
|
|
|||
Loading…
Reference in New Issue