- 创建圈子接口调用

- 充值弹窗
- 充值页面  70%
This commit is contained in:
linshujie 2026-06-03 18:33:22 +08:00
parent 2e719cd614
commit c0df6013fc
70 changed files with 1706 additions and 33 deletions

View File

@ -186,6 +186,10 @@
3062E8BC2FCEAC7100CEF511 /* CreateGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8BB2FCEAC7100CEF511 /* CreateGroupVC.swift */; };
3062E8BE2FCEBD0E00CEF511 /* GroupIconListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8BD2FCEBD0E00CEF511 /* GroupIconListVC.swift */; };
3062E8C02FCED7BB00CEF511 /* GroupIconListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8BF2FCED7BB00CEF511 /* GroupIconListView.swift */; };
3062E8C22FCFB86800CEF511 /* CreateGroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8C12FCFB86800CEF511 /* CreateGroupViewModel.swift */; };
3062E8C42FCFC90F00CEF511 /* CreateGroupVipPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8C32FCFC90F00CEF511 /* CreateGroupVipPopView.swift */; };
3062E8C72FCFD02F00CEF511 /* VipRechargeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8C62FCFD02F00CEF511 /* VipRechargeView.swift */; };
3062E8C92FCFD03B00CEF511 /* VipRechargeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8C82FCFD03B00CEF511 /* VipRechargeVC.swift */; };
30A7A9112FCAEE3D00105780 /* GroupListPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */; };
30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */; };
30BAB84F2FCD2FED00C33B5C /* InviteJoinVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */; };
@ -195,6 +199,8 @@
30BAB8652FCD718A00C33B5C /* JoinGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8642FCD718A00C33B5C /* JoinGroupView.swift */; };
30BAB8682FCD750E00C33B5C /* Mask_group@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 30BAB8672FCD750E00C33B5C /* Mask_group@3x.png */; };
30BAB8692FCD750E00C33B5C /* Mask_group@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 30BAB8662FCD750E00C33B5C /* Mask_group@2x.png */; };
30DC18522FD009CD0041DCD1 /* VipExpenseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18512FD009CD0041DCD1 /* VipExpenseModel.swift */; };
30DC18542FD00C4A0041DCD1 /* VipRechargeVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18532FD00C4A0041DCD1 /* VipRechargeVM.swift */; };
C49B37352A45A02C28FF41BA /* Pods_QuickLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1C77B42994F352054070537 /* Pods_QuickLocation.framework */; };
/* End PBXBuildFile section */
@ -386,6 +392,10 @@
3062E8BB2FCEAC7100CEF511 /* CreateGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupVC.swift; sourceTree = "<group>"; };
3062E8BD2FCEBD0E00CEF511 /* GroupIconListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupIconListVC.swift; sourceTree = "<group>"; };
3062E8BF2FCED7BB00CEF511 /* GroupIconListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupIconListView.swift; sourceTree = "<group>"; };
3062E8C12FCFB86800CEF511 /* CreateGroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupViewModel.swift; sourceTree = "<group>"; };
3062E8C32FCFC90F00CEF511 /* CreateGroupVipPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupVipPopView.swift; sourceTree = "<group>"; };
3062E8C62FCFD02F00CEF511 /* VipRechargeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipRechargeView.swift; sourceTree = "<group>"; };
3062E8C82FCFD03B00CEF511 /* VipRechargeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipRechargeVC.swift; sourceTree = "<group>"; };
30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupListPopView.swift; sourceTree = "<group>"; };
30BAB84C2FCD2FDE00C33B5C /* InviteJoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinView.swift; sourceTree = "<group>"; };
30BAB84E2FCD2FED00C33B5C /* InviteJoinVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteJoinVC.swift; sourceTree = "<group>"; };
@ -395,6 +405,8 @@
30BAB8642FCD718A00C33B5C /* JoinGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinGroupView.swift; sourceTree = "<group>"; };
30BAB8662FCD750E00C33B5C /* Mask_group@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Mask_group@2x.png"; sourceTree = "<group>"; };
30BAB8672FCD750E00C33B5C /* Mask_group@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Mask_group@3x.png"; sourceTree = "<group>"; };
30DC18512FD009CD0041DCD1 /* VipExpenseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipExpenseModel.swift; sourceTree = "<group>"; };
30DC18532FD00C4A0041DCD1 /* VipRechargeVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipRechargeVM.swift; sourceTree = "<group>"; };
3E4359082FC48D26003470A5 /* QuickLocation.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickLocation.app; sourceTree = BUILT_PRODUCTS_DIR; };
93647DF3683AA5E71EC2FB1A /* Pods-QuickLocation.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.release.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.release.xcconfig"; sourceTree = "<group>"; };
D1C77B42994F352054070537 /* Pods_QuickLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QuickLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -851,6 +863,7 @@
305A76352FCA8C7000227D26 /* Map */,
305A76392FCA8C7000227D26 /* Mine */,
305A798E2FCAC5F600227D26 /* InviteMember */,
3062E8C52FCFD01000CEF511 /* VipRecharge */,
3062E8B32FCE6BA400CEF511 /* Scan */,
);
path = Section;
@ -1032,12 +1045,25 @@
children = (
3062E8BB2FCEAC7100CEF511 /* CreateGroupVC.swift */,
3062E8B92FCEAC6500CEF511 /* CreateGroupView.swift */,
3062E8C12FCFB86800CEF511 /* CreateGroupViewModel.swift */,
3062E8BD2FCEBD0E00CEF511 /* GroupIconListVC.swift */,
3062E8BF2FCED7BB00CEF511 /* GroupIconListView.swift */,
3062E8C32FCFC90F00CEF511 /* CreateGroupVipPopView.swift */,
);
path = CreateGroup;
sourceTree = "<group>";
};
3062E8C52FCFD01000CEF511 /* VipRecharge */ = {
isa = PBXGroup;
children = (
3062E8C82FCFD03B00CEF511 /* VipRechargeVC.swift */,
3062E8C62FCFD02F00CEF511 /* VipRechargeView.swift */,
30DC18532FD00C4A0041DCD1 /* VipRechargeVM.swift */,
30DC18512FD009CD0041DCD1 /* VipExpenseModel.swift */,
);
path = VipRecharge;
sourceTree = "<group>";
};
30BAB84B2FCD2FA400C33B5C /* InviteJoin */ = {
isa = PBXGroup;
children = (
@ -1257,6 +1283,7 @@
305A768A2FCA8C7000227D26 /* Single+Response.swift in Sources */,
305A768B2FCA8C7000227D26 /* API.swift in Sources */,
305A768C2FCA8C7000227D26 /* APIProvider.swift in Sources */,
3062E8C92FCFD03B00CEF511 /* VipRechargeVC.swift in Sources */,
305A768D2FCA8C7000227D26 /* AppNetworkConfig.swift in Sources */,
305A768E2FCA8C7000227D26 /* SignPlugin.swift in Sources */,
305A768F2FCA8C7000227D26 /* SystemAPI.swift in Sources */,
@ -1272,6 +1299,7 @@
305A76992FCA8C7000227D26 /* ImagePickerPopup.swift in Sources */,
305A769A2FCA8C7000227D26 /* PopupAnimator.swift in Sources */,
3062E8BE2FCEBD0E00CEF511 /* GroupIconListVC.swift in Sources */,
3062E8C22FCFB86800CEF511 /* CreateGroupViewModel.swift in Sources */,
305A769B2FCA8C7000227D26 /* PopupAnimators.swift in Sources */,
305A769C2FCA8C7000227D26 /* PopupViewController.swift in Sources */,
305A769D2FCA8C7000227D26 /* PopupViewController+Extension.swift in Sources */,
@ -1287,6 +1315,7 @@
305A76A62FCA8C7000227D26 /* Int+Extension.swift in Sources */,
30A7A9112FCAEE3D00105780 /* GroupListPopView.swift in Sources */,
305A76A72FCA8C7000227D26 /* NSAttributedString+Extension.swift in Sources */,
30DC18542FD00C4A0041DCD1 /* VipRechargeVM.swift in Sources */,
305A76A82FCA8C7000227D26 /* ObjectMapper+Extension.swift in Sources */,
305A76A92FCA8C7000227D26 /* Optional+Extension.swift in Sources */,
305A76AA2FCA8C7000227D26 /* Response+ObjectMapper.swift in Sources */,
@ -1327,6 +1356,7 @@
305A76CC2FCA8C7000227D26 /* FileTools.swift in Sources */,
305A76CD2FCA8C7000227D26 /* Permission.swift in Sources */,
305A76CE2FCA8C7000227D26 /* RouterManager.swift in Sources */,
3062E8C72FCFD02F00CEF511 /* VipRechargeView.swift in Sources */,
305A76CF2FCA8C7000227D26 /* CountDownService.swift in Sources */,
305A76D02FCA8C7000227D26 /* MoneyFormatter.swift in Sources */,
305A76D12FCA8C7000227D26 /* TimeSpecificNotificationManager.swift in Sources */,
@ -1371,6 +1401,7 @@
305A76EF2FCA8C7000227D26 /* MineViewController.swift in Sources */,
305A76F02FCA8C7000227D26 /* MineViewModel.swift in Sources */,
305A76F12FCA8C7000227D26 /* SystemService.swift in Sources */,
30DC18522FD009CD0041DCD1 /* VipExpenseModel.swift in Sources */,
305A76F22FCA8C7000227D26 /* UserService.swift in Sources */,
305A76F32FCA8C7000227D26 /* AutoLayout+NSLayoutConstraint.swift in Sources */,
305A76F42FCA8C7000227D26 /* AutoLayout+UIView.swift in Sources */,
@ -1395,6 +1426,7 @@
305A77062FCA8C7000227D26 /* MXScrollViewController.m in Sources */,
305A77072FCA8C7000227D26 /* Helper.swift in Sources */,
305A77082FCA8C7000227D26 /* PageCollectionViewFlowLayout.swift in Sources */,
3062E8C42FCFC90F00CEF511 /* CreateGroupVipPopView.swift in Sources */,
305A77092FCA8C7000227D26 /* PageContentView.swift in Sources */,
305A770A2FCA8C7000227D26 /* PageStyle.swift in Sources */,
305A770B2FCA8C7000227D26 /* PageTitleView.swift in Sources */,

View File

@ -125,6 +125,9 @@ enum GatewayStatusCode: Int {
case noAuthority = 500
//
case review = 201
/** ============== ============== */
//
case groupLimit = 20009
/** ============== ============== */
case unknownError = -9999

View File

@ -18,7 +18,10 @@ enum SystemAPI {
/// - phone:
case sendCode(phone: String)
///
/// - Parameters:
/// - type: member
case rechargeInfo(type: String)
}
extension SystemAPI: MultiTargetProtocol {
@ -29,12 +32,14 @@ extension SystemAPI: MultiTargetProtocol {
return "api/user/config"
case .sendCode:
return "api/user/sms/code"
case .rechargeInfo:
return "api/order/goods"
}
}
var method: Moya.Method {
switch self {
case .userConfig:
case .userConfig, .rechargeInfo:
return .get
case .sendCode:
return .post
@ -50,6 +55,11 @@ extension SystemAPI: MultiTargetProtocol {
var params = Parameters()
params["phone"] = phone
return .requestParameters(parameters: params, encoding: JSONEncoding())
case let .rechargeInfo(type):
var params = Parameters()
params["type"] = type
return .requestParameters(parameters: params, encoding: URLEncoding())
}
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "upgrade_bg@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "upgrade_bg@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: 88 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "wechat@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "wechat@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: 2.2 KiB

View File

@ -101,10 +101,10 @@ extension ApiManager {
handlePopView(message)
default:
///
// let code = GatewayStatusCode(rawValue: code ?? -9999) ?? .unknownError
// if code == .unknownError, handle {
let code = GatewayStatusCode(rawValue: code ?? -9999) ?? .unknownError
if code == .unknownError, handle {
DLToast.show(text: combineMessage)
// }
}
}
return .failure(handleError(with: code, domain: "Data Error", message: combineMessage, data: response))
case let .failure(error):

View File

@ -23,6 +23,8 @@ enum Route: String {
case scan = "scan"
///
case groupIconList = "groupIconList"
///
case vipRecharge = "vipRecharge"
}
extension Route: RouterTarget {
@ -127,11 +129,16 @@ extension AppRouter: AppRouterProtocol {
ScanVC()
}
// MARK: -
// MARK: -
AppRouter.register(Route.groupIconList) { url, parameters in
let vc = GroupIconListVC(iconIndex: parameters["iconIndex"].safeString)
return vc
}
// MARK: -
AppRouter.register(Route.vipRecharge) { url, parameters in
VipRechargeVC()
}
}
}

View File

@ -8,6 +8,7 @@
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
class CreateGroupVC: BaseViewController {
@ -17,16 +18,57 @@ class CreateGroupVC: BaseViewController {
rootView = CreateGroupView(frame: UIScreen.main.bounds)
view = rootView
}
private var viewModel = CreateGroupViewModel()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
reactiveAction()
viewModel.loadData()
}
private func reactiveAction() {
rootView.groupIconInputView.rx.tapGesture.subscribe { _ in
let vc = GroupIconListVC(iconIndex: "1")
vc.onSelectIcon = { index in
self.viewModel.iconIndex = index
self.rootView.groupIconImgView.image = UIImage(named: "GroupIcon/\(index)")
}
self.navigationController?.pushViewController(vc, animated: true)
}.disposed(by: disposeBag)
rootView.submitBtn.rx.tap.subscribe(onNext: { _ in
self.viewModel.requestCreateGroup()
}).disposed(by: disposeBag)
}
private func bindViewModel() {
rootView.groupNameTF.rx.text.orEmpty
.bind(to: viewModel.groupName)
.disposed(by: disposeBag)
rootView.groupContentTV.rx.text.orEmpty
.bind(to: viewModel.groupDesc)
.disposed(by: disposeBag)
viewModel.output.sectionedItems
.bind(to: rootView.tagView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
rootView.tagView.rx.modelSelected(String.self)
.subscribe(viewModel.cellAction.inputs)
.disposed(by: disposeBag)
}
// MARK: - dataSource
private lazy var dataSource: RxCollectionViewSectionedReloadDataSource<GroupTagListSectionModel> = {
RxCollectionViewSectionedReloadDataSource<GroupTagListSectionModel> { datasource, collectionView, indexPath, item in
let cell: TagCell = collectionView.dequeueReusableCell(for: indexPath)
cell.configure(item, isSelected: self.viewModel.isSelected(tag: item))
return cell
}
}()
}

View File

@ -14,8 +14,6 @@ class CreateGroupView: UIView {
var disposeBag = DisposeBag()
private let limitCount = 50
private let tagList = ["私密", "游戏", "运动", "美食",
"自驾", "聚会", "旅行", "学习"]
private func setupRx() {
groupNameTF.rx.text.orEmpty
@ -385,9 +383,7 @@ class CreateGroupView: UIView {
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .clear
cv.isScrollEnabled = false
cv.register(TagCell.self, forCellWithReuseIdentifier: TagCell.reuseId)
cv.delegate = self
cv.dataSource = self
cv.register(TagCell.self)
return cv
}()
@ -415,7 +411,6 @@ class CreateGroupView: UIView {
backgroundColor = .white
setupUI()
setupRx()
tagView.reloadData()
}
required init?(coder aDecoder: NSCoder) {
@ -423,26 +418,6 @@ class CreateGroupView: UIView {
}
}
// MARK: - UICollectionViewDelegate, UICollectionViewDataSource
extension CreateGroupView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return tagList.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TagCell.reuseId, for: indexPath) as! TagCell
cell.configure(tagList[indexPath.item], isSelected: false)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TagCell {
cell.toggleSelection()
}
}
}
// MARK: - TagCell
final class TagCell: UICollectionViewCell {
static let reuseId = "TagCell"

View File

@ -0,0 +1,105 @@
//
// CreateGroupViewModel.swift
// QuickLocation
//
// Created by on 2026/6/3.
//
import RxSwift
import RxCocoa
import RxDataSources
import SwiftyUserDefaults
typealias GroupTagListSectionModel = SectionModel<String, String>
class CreateGroupViewModel {
struct Input {
}
struct Output {
var sectionedItems: Observable<[GroupTagListSectionModel]>
}
let input: Input
let output: Output
var disposeBag = DisposeBag()
private let sectionedItems = PublishSubject<[GroupTagListSectionModel]>()
private let tagList = ["私密", "游戏", "运动", "美食",
"自驾", "聚会", "旅行", "学习"]
var selectedTagList: [String] = [] {
didSet {
loadData()
}
}
var iconIndex = 1
let groupName = BehaviorRelay<String>(value: "")
let groupDesc = BehaviorRelay<String>(value: "")
// MARK: - Cell
lazy var cellAction: Action<String, Void> = { this in
return Action { tag in
if let idx = this.selectedTagList.firstIndex(of: tag) {
this.selectedTagList.remove(at: idx)
} else {
this.selectedTagList.append(tag)
}
return .empty()
}
}(self)
/// tag
func isSelected(tag: String) -> Bool {
return selectedTagList.first(where: { tag == $0 }) != nil
}
// MARK: -
func loadData() {
sectionedItems.onNext(tagList.mapSection())
}
// MARK: - Request
func requestCreateGroup() {
let name = groupName.value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty else {
DLToast.show(text: "请输入圈子名称")
return
}
let desc = groupDesc.value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !desc.isEmpty else {
DLToast.show(text: "请输入圈子描述")
return
}
DLToast.showLoading()
GroupService.operate(opType: "create",
requestData: ["group_name": name,
"icon_index": iconIndex,
"description": desc,
"labels": selectedTagList]).subscribe(onNext: { response in
DLToast.showSuccess(text: "创建成功") {
AppRouter.shared.popOrDismiss()
}
}, onError: { (error) in
guard let code = error.underlyingError?.code else { return }
if code == 20009 { // ""
CreateGroupVipPopView.show()
}
else {
DLToast.show(text: error.localizedDescription)
}
}).disposed(by: disposeBag)
}
// MARK: - init
init() {
input = Input()
output = Output(
sectionedItems: sectionedItems.asObservable()
)
}
}

View File

@ -0,0 +1,119 @@
//
// CreateGroupVipPopView.swift
// QuickLocation
//
// Created by on 2026/6/3.
//
import UIKit
import RxSwift
import RxCocoa
class CreateGroupVipPopView: UIView {
private static let shared = CreateGroupVipPopView(frame: CGRect(origin: .zero, size: kScreenSize))
var disposeBag = DisposeBag()
static func show() {
guard let superView = kKeyWindow else {
return
}
if CreateGroupVipPopView.shared.superview != nil {
CreateGroupVipPopView.shared.removeFromSuperview()
CreateGroupVipPopView.shared.bgView.frame = .zero
}
CreateGroupVipPopView.shared.bgView.alpha = 1
CreateGroupVipPopView.shared.bgView.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
superView.addSubview(CreateGroupVipPopView.shared)
superView.bringSubviewToFront(CreateGroupVipPopView.shared)
UIView.animate(withDuration: 0.25) {
CreateGroupVipPopView.shared.bgView.alpha = 1
}
}
///
static func dismiss() {
guard CreateGroupVipPopView.shared.superview != nil else { return }
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) {
CreateGroupVipPopView.shared.bgView.alpha = 0
} completion: { _ in
CreateGroupVipPopView.shared.removeFromSuperview()
}
}
private func setupRx() {
upgradedBtn.rx.tap.subscribe(onNext: { _ in
CreateGroupVipPopView.dismiss()
AppRouter.push(Route.vipRecharge)
}).disposed(by: disposeBag)
closeBtn.rx.tap.subscribe(onNext: { _ in
CreateGroupVipPopView.dismiss()
}).disposed(by: disposeBag)
}
private lazy var bgView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.5)
return view
}()
lazy var vipImgView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "Group/vip_pop")
return view
}()
lazy var upgradedBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.backgroundColor = .clear
btn.setBackgroundImage(UIImage(named: "Group/upgrade_bg"), for: .normal)
return btn
}()
lazy var closeBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.backgroundColor = .clear
btn.setBackgroundImage(UIImage(named: "Group/close"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 100, bottom: 100, right: 100)
return btn
}()
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
addSubview(bgView)
bgView.addSubview(vipImgView)
bgView.addSubview(upgradedBtn)
bgView.addSubview(closeBtn)
vipImgView.layoutChain
.centerY()
.edgesHorzontal(25)
.heightToWidth(814/648)
upgradedBtn.layoutChain
.topToBottomOfView(vipImgView, offset: -20)
.centerX()
.width(240)
.height(60)
closeBtn.layoutChain
.topToBottomOfView(upgradedBtn, offset: 15)
.centerX()
.width(22)
.height(22)
setupRx()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -6,6 +6,8 @@
//
import UIKit
import RxSwift
import RxCocoa
class GroupIconListVC: BaseViewController {
@ -23,6 +25,13 @@ class GroupIconListVC: BaseViewController {
super.viewDidLoad()
rootView.selectedIndex = iconIndex.integer
rootView.iconCollectionView.delegate = self
rootView.doneBtn.rx.tap.subscribe(onNext: { _ in
if let onSelectIcon = self.onSelectIcon {
onSelectIcon(self.rootView.selectedIndex)
AppRouter.shared.popOrDismiss()
}
}).disposed(by: disposeBag)
}
// MARK: - Init

View File

@ -34,6 +34,7 @@ class GroupIconListView: UIView {
addSubview(navBarView)
navBarView.addSubview(navTitleLabel)
navBarView.addSubview(backBtn)
navBarView.addSubview(doneBtn)
addSubview(selectedIconView)
addSubview(titleLab)
addSubview(iconCollectionView)
@ -57,6 +58,10 @@ class GroupIconListView: UIView {
.width(24)
.height(24)
doneBtn.layoutChain
.centerY(navTitleLabel)
.right(15)
selectedIconView.layoutChain
.topToBottomOfView(navBarView, offset: 30)
.centerX()
@ -71,6 +76,8 @@ class GroupIconListView: UIView {
.topToBottomOfView(titleLab, offset: 13)
.edgesHorzontal(40)
.bottom(kSafeBottomMargin + 10)
doneBtn.sizeToFit()
}
lazy var navBgView: UIImageView = {
@ -102,6 +109,15 @@ class GroupIconListView: UIView {
return btn
}()
lazy var doneBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("完成", for: .normal)
btn.setTitleColor(ThemeManager.shared.color.titleAuxColor, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 100, bottom: 100, right: 15)
return btn
}()
lazy var selectedIconView: UIImageView = {
let view = UIImageView()
view.cornerRadius = 40

View File

@ -0,0 +1,88 @@
//
// VipExpenseModel.swift
// QuickLocation
//
// Created by on 2026/6/3.
//
import ObjectMapper
import RxDataSources
struct VipExpenseResponse: BaseModelProtocol {
//
var code: String?
//
var message: String?
//
var list: [VipExpenseModel] = []
init?(map: Map) {}
mutating func mapping(map: Map) {
code <- map["code"]
message <- map["msg"]
list <- map["data"]
}
}
struct VipExpenseModel: Mappable, Equatable {
///
var goods_id: String = ""
///
var goods_name: String = ""
///
var price: String = ""
///
var origin_price: String = ""
///
var tips: String = ""
///
var tips2: String = ""
/// 1m 12m
var value: String = ""
var unit: String {
guard value.hasSuffix("m") else { return "" }
let monthStr = value.replacingOccurrences(of: "m", with: "")
guard !monthStr.isEmpty, let month = Int(monthStr) else { return "" }
if month >= 12 {
let num = month / 12
return num > 1 ? "/\(num)" : "/年"
} else if month % 3 == 0 {
let num = month / 3
return num > 1 ? "/\(num)" : "/季"
} else {
return month > 1 ? "/\(month)" : "/月"
}
}
///
var pay_type: String = ""
///
var checked: Bool = false
init?(map: Map) {
}
mutating func mapping(map: Map) {
goods_id <- map["goods_id"]
goods_name <- map["goods_name"]
price <- map["price"]
origin_price <- map["origin_price"]
tips <- map["tips"]
tips2 <- map["tips2"]
value <- map["value"]
checked <- map["checked"]
pay_type <- map["pay_type"]
}
}
extension VipExpenseModel: IdentifiableType {
public typealias Identity = String
public var identity: String {
return goods_id
}
}

View File

@ -0,0 +1,71 @@
//
// VipRechargeVC.swift
// QuickLocation
//
// Created by on 2026/6/3.
//
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
class VipRechargeVC: BaseViewController {
fileprivate var rootView: VipRechargeView!
override func loadView() {
rootView = VipRechargeView(frame: UIScreen.main.bounds)
view = rootView
}
private var viewModel = VipRechargeVM()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
reactiveAction()
requestRechargeInfo()
}
private func bindViewModel() {
viewModel.output.sectionedItems
.bind(to: rootView.expenseCollectionView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
private func reactiveAction() {
Observable.zip(
rootView.expenseCollectionView.rx.itemSelected,
rootView.expenseCollectionView.rx.modelSelected(VipExpenseModel.self)
).subscribe(onNext: { indexPath, model in
self.viewModel.selectedIndex = indexPath.row
self.rootView.setupPayTypes(model.pay_type)
self.rootView.animatePrice(to: model.price)
self.rootView.discountLab.text = self.viewModel.discountPriceString
self.viewModel.refreshData()
})
.disposed(by: disposeBag)
}
// MARK: - dataSource
private lazy var dataSource: RxCollectionViewSectionedReloadDataSource<ExpenseListSectionModel> = {
RxCollectionViewSectionedReloadDataSource<ExpenseListSectionModel> { datasource, collectionView, indexPath, model in
let cell: ExpenseCell = collectionView.dequeueReusableCell(for: indexPath)
cell.configure(model: model, isSelected: self.viewModel.selectedIndex == indexPath.row)
return cell
}
}()
// MARK: - API
private func requestRechargeInfo() {
DLToast.showLoading()
SystemService.rechargeInfo(type: "member").subscribe(onNext: { [weak self] response in
guard let self = self else { return }
self.viewModel.loadData(list: response.list)
self.rootView.setupPayTypes(self.viewModel.payType)
self.rootView.animatePrice(to: self.viewModel.price)
self.rootView.discountLab.text = self.viewModel.discountPriceString
}).disposed(by: disposeBag)
}
}

View File

@ -0,0 +1,72 @@
//
// VipRechargeVM.swift
// QuickLocation
//
// Created by on 2026/6/3.
//
import RxSwift
import RxCocoa
import RxDataSources
import SwiftyUserDefaults
typealias ExpenseListSectionModel = SectionModel<String, VipExpenseModel>
class VipRechargeVM {
struct Input {
}
struct Output {
var sectionedItems: Observable<[ExpenseListSectionModel]>
}
let input: Input
let output: Output
var disposeBag = DisposeBag()
private let sectionedItems = PublishSubject<[ExpenseListSectionModel]>()
var selectedIndex: Int = -1
var list: [VipExpenseModel] = []
var payType: String {
guard list.count > 0 else { return "" }
return list[selectedIndex].pay_type
}
var price: String {
guard list.count > 0 else { return "" }
return list[selectedIndex].price
}
var discountPriceString: String {
guard list.count > 0 else { return "已优惠0元" }
let price = list[selectedIndex].price
let oPrice = list[selectedIndex].origin_price
guard oPrice.int >= price.int else { return "已优惠0元" }
let diff = oPrice.double - price.double
let diffStr = diff.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f", diff) : String(diff)
return "已优惠\(diffStr)"
}
// MARK: -
func loadData(list: [VipExpenseModel]) {
self.list = list
selectedIndex = list.firstIndex(where: { $0.checked == true }) ?? 0
sectionedItems.onNext(list.mapSection())
}
func refreshData() {
sectionedItems.onNext(list.mapSection())
}
// MARK: - init
init() {
input = Input()
output = Output(
sectionedItems: sectionedItems.asObservable()
)
}
}

View File

@ -0,0 +1,684 @@
//
// VipRechargeView.swift
// QuickLocation
//
// Created by on 2026/6/3.
//
import UIKit
import RxSwift
import RxCocoa
class VipRechargeView: UIView {
var disposeBag = DisposeBag()
private func setupRx() {
backBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.shared.popOrDismiss()
}).disposed(by: disposeBag)
agreementLab.rx.tapGesture.subscribe { _ in
// TODO:
}.disposed(by: disposeBag)
}
private func setupUI() {
addSubview(scrollView)
scrollView.addSubview(scrollContentView)
scrollContentView.addSubview(headerBgImgView)
scrollContentView.addSubview(cornerView)
scrollContentView.addSubview(expenseCollectionView)
scrollContentView.addSubview(vipRightsView)
scrollContentView.addSubview(agreementLab)
scrollContentView.addSubview(tipsLab)
vipRightsView.addSubview(vipRightsTitleView)
addSubview(bottomView)
bottomView.addSubview(payTypeStackView)
bottomView.addSubview(payBtnView)
bottomView.addSubview(payPriceView)
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)
bottomView.layoutChain
.edgesHorzontal()
.heightToWidth(144/375)
.bottom()
payTypeStackView.layoutChain
.top(25)
.edgesHorzontal(16)
.centerX()
payBtnView.layoutChain
.edgesHorzontal(16)
.heightToWidth(50/343)
.centerY()
payPriceView.layoutChain
.left(32)
.centerY(payBtnView, offset: -7)
.height(30)
scrollView.layoutChain
.edges(excludingEdge: .bottom)
.bottomToTopOfView(bottomView)
scrollContentView.layoutChain
.edges()
.widthToView(scrollView)
headerBgImgView.layoutChain
.top(-kStatusBarHeight)
.edgesHorzontal()
// .edges(excludingEdge: .bottom)
.heightToWidth(267/375)
cornerView.layoutChain
.topToBottomOfView(headerBgImgView, offset: -20)
.edges(excludingEdge: .top)
let expenseCollectionViewHeight = (kScreenWidth - 32 - 32) / 3 * (144/110) + 10
expenseCollectionView.layoutChain
.topToView(cornerView, offset: -43)
.edgesHorzontal()
.height(expenseCollectionViewHeight * 1.1)
vipRightsView.layoutChain
.topToBottomOfView(expenseCollectionView, offset: 18)
.edgesHorzontal(16)
.height(302)
vipRightsTitleView.layoutChain
.top(14)
.centerX()
agreementLab.layoutChain
.topToBottomOfView(vipRightsView, offset: 6)
.leftToView(vipRightsView)
.rightToView(vipRightsView)
tipsLab.layoutChain
.topToBottomOfView(agreementLab, offset: 4)
.leftToView(vipRightsView)
.rightToView(vipRightsView)
.bottom(10)
}
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: 100, right: 100)
return btn
}()
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.backgroundColor = UIColor(hexStr: "#F5FBFB")
view.showsVerticalScrollIndicator = false
view.delegate = self
view.bounces = false
return view
}()
lazy var scrollContentView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var headerBgImgView: UIImageView = {
let view = UIImageView()
view.image = UIImage(named: "VipRecharge/header_bg")
return view
}()
lazy var cornerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#F5FBFB")
view.clipsToBounds = false
return view
}()
///
lazy var expenseCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let spacing: CGFloat = 16
let cvWidth = kScreenWidth - 32
let itemW = (cvWidth - spacing * 2) / 3
layout.itemSize = CGSize(width: itemW, height: itemW * (144/110) + 10)
layout.sectionInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
layout.minimumLineSpacing = spacing
layout.scrollDirection = .horizontal
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .clear
cv.showsHorizontalScrollIndicator = false
cv.register(ExpenseCell.self)
return cv
}()
///
lazy var vipRightsView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 10
return view
}()
lazy var vipRightsTitleView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let titleLab = UILabel()
titleLab.text = "会员权益"
titleLab.font = .systemFont(ofSize: 14, weight: .bold)
titleLab.textColor = UIColor(hexStr: "#3D3D3D")
titleLab.textAlignment = .center
view.addSubview(titleLab)
titleLab.layoutChain
.edgesVertical()
.centerX()
let leftLine1 = UIView()
leftLine1.backgroundColor = UIColor(hexStr: "#B7F34E")
leftLine1.cornerRadius = 1
view.addSubview(leftLine1)
leftLine1.layoutChain
.left()
.width(2)
.height(10)
.centerY()
let leftLine2 = UIView()
leftLine2.backgroundColor = UIColor(hexStr: "#B7F34E")
leftLine2.cornerRadius = 1
view.addSubview(leftLine2)
leftLine2.layoutChain
.leftToRightOfView(leftLine1, offset: 4)
.width(2)
.edgesVertical()
.rightToLeftOfView(titleLab, offset: -10)
let leftLine3 = UIView()
leftLine3.backgroundColor = UIColor(hexStr: "#B7F34E")
leftLine3.cornerRadius = 1
view.addSubview(leftLine3)
leftLine3.layoutChain
.leftToRightOfView(titleLab, offset: 10)
.width(2)
.edgesVertical()
let leftLine4 = UIView()
leftLine4.backgroundColor = UIColor(hexStr: "#B7F34E")
leftLine4.cornerRadius = 1
view.addSubview(leftLine4)
leftLine4.layoutChain
.leftToRightOfView(leftLine3, offset: 4)
.width(2)
.height(10)
.centerY()
return view
}()
lazy var groupCountView: UIView = {
let view = UIView()
view.backgroundColor = .clear
//
let createCountView = UIView()
createCountView.backgroundColor = .clear
let createIcon = UIImageView(image: UIImage(named: "VipRecharge/create_count"))
let createBgImg = UIImageView(image: UIImage(named: "VipRecharge/count_bg"))
let createUnitLab = UILabel()
createUnitLab.text = ""
createUnitLab.font = .systemFont(ofSize: 10, weight: .medium)
createUnitLab.textColor = UIColor(hexStr: "#1A1A1A")
let createTitleLab = UILabel()
createTitleLab.text = "创建圈子"
createTitleLab.font = .systemFont(ofSize: 12, weight: .medium)
createTitleLab.textColor = UIColor(hexStr: "#1A1A1A")
createCountView.addSubview(createIcon)
createCountView.addSubview(createBgImg)
createCountView.addSubview(createUnitLab)
createCountView.addSubview(createTitleLab)
createCountView.addSubview(createCountLab)
createIcon.layoutChain
.top()
.centerX()
.width(34).height(34)
createBgImg.layoutChain
.top(8)
createCountLab.layoutChain
.topToBottomOfView(createIcon)
// 线
let separator1 = UIImageView(image: UIImage(named: "VipRecharge/separator"))
//
let separator2 = UIImageView(image: UIImage(named: "VipRecharge/separator"))
return view
}()
lazy var createCountLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textColor = UIColor(hexStr: "#FF4F44")
return label
}()
lazy var agreementLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 10, weight: .medium)
label.textColor = UIColor(hexStr: "#767676")
label.isUserInteractionEnabled = true
let text = "*开通前请阅读《会员服务协议》"
let attr = NSMutableAttributedString(string: text)
let range = (text as NSString).range(of: "《会员服务协议》")
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#2DBBFF"), range: range)
label.attributedText = attr
return label
}()
lazy var tipsLab: UILabel = {
let label = UILabel()
label.text = "*根据相关隐私保护的规定,本产品功能需双方下载并授权同意后再使用,请在自己的设备上使用,不得在未经过对方同意和授权的情况下使用,仅限家庭/亲人/朋友/情侣等熟人间使用。"
label.font = .systemFont(ofSize: 10, weight: .medium)
label.textColor = UIColor(hexStr: "#767676")
label.numberOfLines = 0
return label
}()
lazy var payTypeStackView: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.spacing = 30
// stack.distribution = .fillEqually
stack.alignment = .center
return stack
}()
var selectedPayTypeTag: Int = 0
/// pay_type (: "alipay,weixin")
func setupPayTypes(_ payTypeStr: String) {
payTypeStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
let types = payTypeStr.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) }
let payTypeMap: [(String, String)] = types.compactMap {
if $0 == "weixin" { return ("wechat", "微信支付") }
if $0 == "alipay" { return ("alipay", "支付宝支付") }
return nil
}
guard !payTypeMap.isEmpty else { return }
selectedPayTypeTag = 0
for (idx, (icon, name)) in payTypeMap.enumerated() {
let isSelected = idx == 0
let view = makePayTypeView(tag: idx, icon: icon, name: name, isSelected: isSelected)
payTypeStackView.addArrangedSubview(view)
}
}
private func makePayTypeView(tag: Int, icon: String, name: String, isSelected: Bool) -> UIView {
let view = UIView()
view.tag = tag
view.isUserInteractionEnabled = true
let checkbox = UIImageView(image: UIImage(named: isSelected ? "VipRecharge/checkbox_on" : "VipRecharge/checkbox"))
checkbox.contentMode = .scaleAspectFit
checkbox.tag = 100
view.addSubview(checkbox)
checkbox.layoutChain.left().centerY().width(18).height(18)
let payIcon = UIImageView(image: UIImage(named: "VipRecharge/\(icon)"))
payIcon.contentMode = .scaleAspectFit
view.addSubview(payIcon)
payIcon.layoutChain.leftToRightOfView(checkbox, offset: 8).centerY().width(22).height(22)
let nameLab = UILabel()
nameLab.text = name
nameLab.font = .systemFont(ofSize: 14, weight: .medium)
nameLab.textColor = UIColor(hexStr: "#1A1A1A")
view.addSubview(nameLab)
nameLab.layoutChain.leftToRightOfView(payIcon, offset: 6).centerY()
let tap = UITapGestureRecognizer(target: self, action: #selector(onPayTypeTap(_:)))
view.addGestureRecognizer(tap)
return view
}
@objc private func onPayTypeTap(_ gesture: UITapGestureRecognizer) {
guard let tag = gesture.view?.tag, tag != selectedPayTypeTag else { return }
selectedPayTypeTag = tag
for view in payTypeStackView.arrangedSubviews {
let isSelected = view.tag == tag
if let checkbox = view.viewWithTag(100) as? UIImageView {
checkbox.image = UIImage(named: isSelected ? "VipRecharge/checkbox_on" : "VipRecharge/checkbox")
}
}
}
lazy var bottomView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: -3)
view.layer.shadowOpacity = 0.06
return view
}()
lazy var payBtnView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let bgImg = UIImageView()
bgImg.image = UIImage(named: "VipRecharge/pay_bg")
view.addSubview(bgImg)
bgImg.layoutChain.edges()
let unlockLab = UILabel()
unlockLab.text = "解锁会员"
unlockLab.font = .systemFont(ofSize: 16, weight: .heavy)
unlockLab.textColor = .white
view.addSubview(unlockLab)
unlockLab.layoutChain.right(21).centerY()
return view
}()
lazy var payPriceView: UIView = {
let view = UIView()
view.backgroundColor = .clear
let titleLab = UILabel()
titleLab.text = "合计"
titleLab.font = .systemFont(ofSize: 14, weight: .bold)
titleLab.textColor = UIColor(hexStr: "#1A1A1A")
view.addSubview(titleLab)
titleLab.layoutChain
.left()
.bottom()
let symbolLab = UILabel()
symbolLab.text = ""
symbolLab.font = .systemFont(ofSize: 12, weight: .heavy)
symbolLab.textColor = UIColor(hexStr: "#FF3B05")
view.addSubview(symbolLab)
symbolLab.layoutChain
.leftToRightOfView(titleLab)
.bottomToView(titleLab)
view.addSubview(priceLab)
priceLab.layoutChain
.leftToRightOfView(symbolLab)
.bottomToView(titleLab, offset: 3)
view.addSubview(discountLab)
discountLab.layoutChain
.leftToRightOfView(priceLab, offset: 5)
.bottomToView(titleLab)
.right()
return view
}()
lazy var priceLab: UILabel = {
let label = UILabel()
label.text = "0"
label.font = .systemFont(ofSize: 24, weight: .heavy)
label.textColor = UIColor(hexStr: "#FF3B05")
return label
}()
///
func animatePrice(to newValue: String) {
guard let target = Double(newValue) else {
priceLab.text = newValue
return
}
let current = Double(priceLab.text ?? "0") ?? 0
let diff = target - current
let duration = 0.6
let steps = 30
var step = 0
Timer.scheduledTimer(withTimeInterval: duration / Double(steps), repeats: true) { [weak self] t in
step += 1
guard let self = self else { t.invalidate(); return }
if step >= steps {
t.invalidate()
self.priceLab.text = newValue
} else {
let progress = Double(step) / Double(steps)
// ease out + overshoot on increase
let eased = diff > 0
? 1 - pow(1 - progress, 3)
: pow(progress, 0.5)
let value = current + diff * eased
self.priceLab.text = String(format: "%.0f", value)
}
}
}
lazy var discountLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14, weight: .bold)
label.textColor = UIColor(hexStr: "#1A1A1A")
return label
}()
override func layoutSubviews() {
super.layoutSubviews()
cornerView.setNeedsLayout()
cornerView.layoutIfNeeded()
cornerView.setCornerRadius(corners: [.topLeft, .topRight], withCornerRadii: CGSize(width: 10, height: 10))
}
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: - ExpenseCell
final class ExpenseCell: UICollectionViewCell {
private let tagBadgeView: UIView = {
let iv = UIView()
// iv.image = UIImage(named: "VipRecharge/expense_tips")
// iv.contentMode = .scaleAspectFill
iv.backgroundColor = UIColor(hexStr: "#FF6643")
iv.isHidden = true
return iv
}()
private let tagLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 10, weight: .bold)
label.textColor = .white
label.textAlignment = .center
return label
}()
private let bgImgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "VipRecharge/expense")
iv.contentMode = .scaleAspectFill
return iv
}()
private let titleLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14, weight: .bold)
label.textColor = UIColor(hexStr: "#1A1A1A")
label.textAlignment = .center
return label
}()
lazy var priceLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12, weight: .bold)
label.textColor = UIColor(hexStr: "#1A1A1A")
label.textAlignment = .center
return label
}()
lazy var originPriceLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12, weight: .bold)
label.textColor = UIColor(hexStr: "#767676")
label.textAlignment = .center
return label
}()
lazy var tipsLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 10, weight: .bold)
label.textColor = UIColor(hexStr: "#1A1A1A")
label.textAlignment = .center
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(bgImgView)
contentView.addSubview(tagBadgeView)
tagBadgeView.addSubview(tagLabel)
contentView.addSubview(titleLab)
contentView.addSubview(priceLab)
contentView.addSubview(originPriceLab)
contentView.addSubview(tipsLab)
tagBadgeView.layoutChain
.top().left()
tagLabel.layoutChain
.edgesHorzontal(10)
.edgesVertical(3)
bgImgView.layoutChain
.top(10)
.left().right().bottom()
titleLab.layoutChain
.centerX()
.top(30)
.edgesHorzontal(2)
priceLab.layoutChain
.centerY()
.edgesHorzontal(2)
originPriceLab.layoutChain
.topToBottomOfView(priceLab, offset: 5)
.edgesHorzontal(2)
tipsLab.layoutChain
.edgesHorzontal(2)
.bottom(7)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(model: VipExpenseModel, isSelected: Bool) {
titleLab.text = model.goods_name
tagBadgeView.isHidden = model.tips.isEmpty
tagLabel.text = model.tips
tipsLab.text = model.tips2
let priceAttr = NSMutableAttributedString(string: "", attributes: [.font: UIFont.systemFont(ofSize: 12, weight: .medium)])
priceAttr.append(NSAttributedString(string: model.price, attributes: [.font: UIFont.systemFont(ofSize: 30, weight: .medium)]))
priceAttr.append(NSAttributedString(string: model.unit, attributes: [.font: UIFont.systemFont(ofSize: 12, weight: .medium)]))
priceLab.attributedText = priceAttr
originPriceLab.text = "" + model.origin_price
originPriceLab.setupStrikethroughStyle()
setSelected(isSelected, animated: false)
}
func setSelected(_ selected: Bool, animated: Bool) {
let scale: CGFloat = selected ? 1.1 : 1.0
let imageName = selected ? "VipRecharge/expense_on" : "VipRecharge/expense"
bgImgView.image = UIImage(named: imageName)
titleLab.textColor = selected ? UIColor(hexStr: "#16B3FF") : UIColor(hexStr: "#1A1A1A")
priceLab.textColor = selected ? UIColor(hexStr: "#FF4F44") : UIColor(hexStr: "#1A1A1A")
let animations = {
self.transform = CGAffineTransform(scaleX: scale, y: scale)
}
if animated {
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut, animations: animations)
} else {
animations()
}
}
override func layoutSubviews() {
super.layoutSubviews()
tagBadgeView.setNeedsLayout()
tagBadgeView.layoutIfNeeded()
tagBadgeView.setCornerRadius(corners: [.topLeft, .bottomRight], withCornerRadii: CGSize(width: 10, height: 10))
}
}
extension VipRechargeView: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let maxY = scrollView.contentOffset.y
let alpha = maxY / kNaviHeight
navBarView.alpha = alpha < 0.0 ? 0.0 : alpha
}
}

View File

@ -0,0 +1,47 @@
//
// VipRechargeViewModel.swift
// QuickLocation
//
// Created by on 2026/6/3.
//
import RxSwift
import RxCocoa
import RxDataSources
class VipRechargeViewModel {
struct Output {
var sectionedItems: Observable<[ExpenseSectionModel]>
}
let output: Output
private let sectionedItems = PublishSubject<[ExpenseSectionModel]>()
private let expenseList: [ExpenseItem] = [
ExpenseItem(title: "白银会员", tag: "推荐"),
ExpenseItem(title: "黄金会员", tag: "热门"),
ExpenseItem(title: "钻石会员", tag: "爆款"),
ExpenseItem(title: "永久会员", tag: "超值")
]
var selectedIndex: Int = 0 {
didSet {
loadData()
}
}
func loadData() {
sectionedItems.onNext([SectionModel(model: "expense", items: expenseList)])
}
func isSelected(at index: Int) -> Bool {
return index == selectedIndex
}
init() {
output = Output(
sectionedItems: sectionedItems.asObservable()
)
}
}

View File

@ -25,4 +25,14 @@ struct SystemService {
.map(SmsCodeResponse.self)
.asObservable()
}
///
/// - Parameters:
/// - type: member
static func rechargeInfo(type: String) -> Observable<VipExpenseResponse> {
let api = SystemAPI.rechargeInfo(type: type).multiTarget
return APIProvider.request(token: api)
.map(VipExpenseResponse.self)
.asObservable()
}
}