- 权限检测

- 审核成员
- 移除成员
- IM警告消息
This commit is contained in:
linshujie 2026-06-12 18:31:32 +08:00
parent 7931a9beb6
commit d5296061c3
42 changed files with 1439 additions and 73 deletions

View File

@ -182,6 +182,12 @@
30BAB8532FCD337C00C33B5C /* GroupService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8522FCD337C00C33B5C /* GroupService.swift */; }; 30BAB8532FCD337C00C33B5C /* GroupService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8522FCD337C00C33B5C /* GroupService.swift */; };
30BAB8632FCD716C00C33B5C /* JoinGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8622FCD716C00C33B5C /* JoinGroupVC.swift */; }; 30BAB8632FCD716C00C33B5C /* JoinGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8622FCD716C00C33B5C /* JoinGroupVC.swift */; };
30BAB8652FCD718A00C33B5C /* JoinGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8642FCD718A00C33B5C /* JoinGroupView.swift */; }; 30BAB8652FCD718A00C33B5C /* JoinGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30BAB8642FCD718A00C33B5C /* JoinGroupView.swift */; };
30C4C0162FDB91B8009215C1 /* CheckPermissionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C0152FDB91B8009215C1 /* CheckPermissionVC.swift */; };
30C4C0192FDBF094009215C1 /* RemoveMemberVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C0182FDBF094009215C1 /* RemoveMemberVC.swift */; };
30C4C01B2FDBF09D009215C1 /* RemoveMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C01A2FDBF09D009215C1 /* RemoveMemberView.swift */; };
30C4C01D2FDBF557009215C1 /* RemoveMemberViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C01C2FDBF557009215C1 /* RemoveMemberViewModel.swift */; };
30C4C0202FDC0EC5009215C1 /* GroupInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C01F2FDC0EC5009215C1 /* GroupInfoView.swift */; };
30C4C0222FDC0ED3009215C1 /* GroupInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30C4C0212FDC0ED3009215C1 /* GroupInfoVC.swift */; };
30DC18522FD009CD0041DCD1 /* VipExpenseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18512FD009CD0041DCD1 /* VipExpenseModel.swift */; }; 30DC18522FD009CD0041DCD1 /* VipExpenseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18512FD009CD0041DCD1 /* VipExpenseModel.swift */; };
30DC18542FD00C4A0041DCD1 /* VipRechargeVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18532FD00C4A0041DCD1 /* VipRechargeVM.swift */; }; 30DC18542FD00C4A0041DCD1 /* VipRechargeVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18532FD00C4A0041DCD1 /* VipRechargeVM.swift */; };
30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18562FD11E7A0041DCD1 /* WebOperations.swift */; }; 30DC185A2FD11E7A0041DCD1 /* WebOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30DC18562FD11E7A0041DCD1 /* WebOperations.swift */; };
@ -408,6 +414,12 @@
30BAB8622FCD716C00C33B5C /* JoinGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinGroupVC.swift; sourceTree = "<group>"; }; 30BAB8622FCD716C00C33B5C /* JoinGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinGroupVC.swift; sourceTree = "<group>"; };
30BAB8642FCD718A00C33B5C /* JoinGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinGroupView.swift; sourceTree = "<group>"; }; 30BAB8642FCD718A00C33B5C /* JoinGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinGroupView.swift; sourceTree = "<group>"; };
30C4C0112FDABC8C009215C1 /* QuickLocation.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QuickLocation.entitlements; sourceTree = "<group>"; }; 30C4C0112FDABC8C009215C1 /* QuickLocation.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QuickLocation.entitlements; sourceTree = "<group>"; };
30C4C0152FDB91B8009215C1 /* CheckPermissionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckPermissionVC.swift; sourceTree = "<group>"; };
30C4C0182FDBF094009215C1 /* RemoveMemberVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveMemberVC.swift; sourceTree = "<group>"; };
30C4C01A2FDBF09D009215C1 /* RemoveMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveMemberView.swift; sourceTree = "<group>"; };
30C4C01C2FDBF557009215C1 /* RemoveMemberViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveMemberViewModel.swift; sourceTree = "<group>"; };
30C4C01F2FDC0EC5009215C1 /* GroupInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInfoView.swift; sourceTree = "<group>"; };
30C4C0212FDC0ED3009215C1 /* GroupInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInfoVC.swift; sourceTree = "<group>"; };
30DC18512FD009CD0041DCD1 /* VipExpenseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipExpenseModel.swift; 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>"; }; 30DC18532FD00C4A0041DCD1 /* VipRechargeVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VipRechargeVM.swift; sourceTree = "<group>"; };
30DC18552FD11E7A0041DCD1 /* NavigationTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTitleView.swift; sourceTree = "<group>"; }; 30DC18552FD11E7A0041DCD1 /* NavigationTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTitleView.swift; sourceTree = "<group>"; };
@ -820,9 +832,11 @@
305A76252FCA8C7000227D26 /* GroupViewModel.swift */, 305A76252FCA8C7000227D26 /* GroupViewModel.swift */,
305A76232FCA8C7000227D26 /* GroupView.swift */, 305A76232FCA8C7000227D26 /* GroupView.swift */,
30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */, 30EFF3B82FD8FC5200EB35D4 /* VerificationPopView.swift */,
30C4C01E2FDC0EA6009215C1 /* GroupInfo */,
307073E42FD18A20004C37CC /* GroupChat */, 307073E42FD18A20004C37CC /* GroupChat */,
30EFF3A22FD7C58400EB35D4 /* GroupSetting */, 30EFF3A22FD7C58400EB35D4 /* GroupSetting */,
30EFF3B12FD8F19E00EB35D4 /* ReviewMemberList */, 30EFF3B12FD8F19E00EB35D4 /* ReviewMemberList */,
30C4C0172FDBF066009215C1 /* RemoveMember */,
3062E8B82FCEAC5600CEF511 /* CreateGroup */, 3062E8B82FCEAC5600CEF511 /* CreateGroup */,
30BAB8612FCD714700C33B5C /* Join */, 30BAB8612FCD714700C33B5C /* Join */,
30BAB84B2FCD2FA400C33B5C /* InviteJoin */, 30BAB84B2FCD2FA400C33B5C /* InviteJoin */,
@ -881,6 +895,7 @@
30EFF3BC2FD9585200EB35D4 /* Account */, 30EFF3BC2FD9585200EB35D4 /* Account */,
30EFF3D42FDA8EF200EB35D4 /* EmergencyContact */, 30EFF3D42FDA8EF200EB35D4 /* EmergencyContact */,
30EFF3E32FDAA92200EB35D4 /* PrivacyPolicy */, 30EFF3E32FDAA92200EB35D4 /* PrivacyPolicy */,
30C4C0122FDB9178009215C1 /* CheckPermission */,
); );
path = Mine; path = Mine;
sourceTree = "<group>"; sourceTree = "<group>";
@ -888,14 +903,14 @@
305A763A2FCA8C7000227D26 /* Section */ = { 305A763A2FCA8C7000227D26 /* Section */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
305A76262FCA8C7000227D26 /* Group */,
305A762A2FCA8C7000227D26 /* Home */, 305A762A2FCA8C7000227D26 /* Home */,
305A762C2FCA8C7000227D26 /* Launch */,
305A76312FCA8C7000227D26 /* Login */,
305A76352FCA8C7000227D26 /* Map */, 305A76352FCA8C7000227D26 /* Map */,
305A76262FCA8C7000227D26 /* Group */,
305A76392FCA8C7000227D26 /* Mine */, 305A76392FCA8C7000227D26 /* Mine */,
305A798E2FCAC5F600227D26 /* InviteMember */, 305A798E2FCAC5F600227D26 /* InviteMember */,
3062E8C52FCFD01000CEF511 /* VipRecharge */, 3062E8C52FCFD01000CEF511 /* VipRecharge */,
305A76312FCA8C7000227D26 /* Login */,
305A762C2FCA8C7000227D26 /* Launch */,
3062E8B32FCE6BA400CEF511 /* Scan */, 3062E8B32FCE6BA400CEF511 /* Scan */,
30DC18592FD11E7A0041DCD1 /* Web */, 30DC18592FD11E7A0041DCD1 /* Web */,
30EFF3AD2FD7FF1400EB35D4 /* TextInput */, 30EFF3AD2FD7FF1400EB35D4 /* TextInput */,
@ -1132,6 +1147,33 @@
path = Join; path = Join;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
30C4C0122FDB9178009215C1 /* CheckPermission */ = {
isa = PBXGroup;
children = (
30C4C0152FDB91B8009215C1 /* CheckPermissionVC.swift */,
);
path = CheckPermission;
sourceTree = "<group>";
};
30C4C0172FDBF066009215C1 /* RemoveMember */ = {
isa = PBXGroup;
children = (
30C4C0182FDBF094009215C1 /* RemoveMemberVC.swift */,
30C4C01C2FDBF557009215C1 /* RemoveMemberViewModel.swift */,
30C4C01A2FDBF09D009215C1 /* RemoveMemberView.swift */,
);
path = RemoveMember;
sourceTree = "<group>";
};
30C4C01E2FDC0EA6009215C1 /* GroupInfo */ = {
isa = PBXGroup;
children = (
30C4C0212FDC0ED3009215C1 /* GroupInfoVC.swift */,
30C4C01F2FDC0EC5009215C1 /* GroupInfoView.swift */,
);
path = GroupInfo;
sourceTree = "<group>";
};
30DC18582FD11E7A0041DCD1 /* Controller */ = { 30DC18582FD11E7A0041DCD1 /* Controller */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1173,8 +1215,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
30EFF3B42FD8F1D000EB35D4 /* ReviewMemberListVC.swift */, 30EFF3B42FD8F1D000EB35D4 /* ReviewMemberListVC.swift */,
30EFF3B22FD8F1C200EB35D4 /* ReviewMemberListView.swift */,
30EFF3B62FD8F86200EB35D4 /* ReviewMemberListVM.swift */, 30EFF3B62FD8F86200EB35D4 /* ReviewMemberListVM.swift */,
30EFF3B22FD8F1C200EB35D4 /* ReviewMemberListView.swift */,
); );
path = ReviewMemberList; path = ReviewMemberList;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1412,6 +1454,7 @@
305A768D2FCA8C7000227D26 /* AppNetworkConfig.swift in Sources */, 305A768D2FCA8C7000227D26 /* AppNetworkConfig.swift in Sources */,
305A768E2FCA8C7000227D26 /* SignPlugin.swift in Sources */, 305A768E2FCA8C7000227D26 /* SignPlugin.swift in Sources */,
305A768F2FCA8C7000227D26 /* SystemAPI.swift in Sources */, 305A768F2FCA8C7000227D26 /* SystemAPI.swift in Sources */,
30C4C0222FDC0ED3009215C1 /* GroupInfoVC.swift in Sources */,
305A76902FCA8C7000227D26 /* UserAPI.swift in Sources */, 305A76902FCA8C7000227D26 /* UserAPI.swift in Sources */,
30EFF3DE2FDA982C00EB35D4 /* EmergencyContactAddVC.swift in Sources */, 30EFF3DE2FDA982C00EB35D4 /* EmergencyContactAddVC.swift in Sources */,
305A76912FCA8C7000227D26 /* Constant.swift in Sources */, 305A76912FCA8C7000227D26 /* Constant.swift in Sources */,
@ -1422,6 +1465,7 @@
305A76942FCA8C7000227D26 /* UploadImageCell.swift in Sources */, 305A76942FCA8C7000227D26 /* UploadImageCell.swift in Sources */,
305A76952FCA8C7000227D26 /* CornerRadiusCell.swift in Sources */, 305A76952FCA8C7000227D26 /* CornerRadiusCell.swift in Sources */,
305A76962FCA8C7000227D26 /* CornerRadiusFooterView.swift in Sources */, 305A76962FCA8C7000227D26 /* CornerRadiusFooterView.swift in Sources */,
30C4C0162FDB91B8009215C1 /* CheckPermissionVC.swift in Sources */,
305A76972FCA8C7000227D26 /* CornerRadiusHeaderView.swift in Sources */, 305A76972FCA8C7000227D26 /* CornerRadiusHeaderView.swift in Sources */,
305A76982FCA8C7000227D26 /* ImagePicker.swift in Sources */, 305A76982FCA8C7000227D26 /* ImagePicker.swift in Sources */,
305A76992FCA8C7000227D26 /* ImagePickerPopup.swift in Sources */, 305A76992FCA8C7000227D26 /* ImagePickerPopup.swift in Sources */,
@ -1478,6 +1522,7 @@
305A76BD2FCA8C7000227D26 /* PaginationModel.swift in Sources */, 305A76BD2FCA8C7000227D26 /* PaginationModel.swift in Sources */,
305A76BE2FCA8C7000227D26 /* ResponseModel.swift in Sources */, 305A76BE2FCA8C7000227D26 /* ResponseModel.swift in Sources */,
30EFF3C02FD958AE00EB35D4 /* AccountVC.swift in Sources */, 30EFF3C02FD958AE00EB35D4 /* AccountVC.swift in Sources */,
30C4C0202FDC0EC5009215C1 /* GroupInfoView.swift in Sources */,
305A76BF2FCA8C7000227D26 /* ListService.swift in Sources */, 305A76BF2FCA8C7000227D26 /* ListService.swift in Sources */,
305A76C02FCA8C7000227D26 /* BaseNavigationController.swift in Sources */, 305A76C02FCA8C7000227D26 /* BaseNavigationController.swift in Sources */,
305A76C12FCA8C7000227D26 /* BaseViewController.swift in Sources */, 305A76C12FCA8C7000227D26 /* BaseViewController.swift in Sources */,
@ -1489,6 +1534,7 @@
305A76C32FCA8C7000227D26 /* MainTabBarController.swift in Sources */, 305A76C32FCA8C7000227D26 /* MainTabBarController.swift in Sources */,
30EFF3CA2FDA575600EB35D4 /* CancellationPopView.swift in Sources */, 30EFF3CA2FDA575600EB35D4 /* CancellationPopView.swift in Sources */,
305A76C42FCA8C7000227D26 /* QuickLocationTabBar.swift in Sources */, 305A76C42FCA8C7000227D26 /* QuickLocationTabBar.swift in Sources */,
30C4C01D2FDBF557009215C1 /* RemoveMemberViewModel.swift in Sources */,
305A76C52FCA8C7000227D26 /* Account.swift in Sources */, 305A76C52FCA8C7000227D26 /* Account.swift in Sources */,
305A76C62FCA8C7000227D26 /* AppContextManager.swift in Sources */, 305A76C62FCA8C7000227D26 /* AppContextManager.swift in Sources */,
305A76C72FCA8C7000227D26 /* UserConfigModel.swift in Sources */, 305A76C72FCA8C7000227D26 /* UserConfigModel.swift in Sources */,
@ -1556,6 +1602,7 @@
305A76F02FCA8C7000227D26 /* MineViewModel.swift in Sources */, 305A76F02FCA8C7000227D26 /* MineViewModel.swift in Sources */,
305A76F12FCA8C7000227D26 /* SystemService.swift in Sources */, 305A76F12FCA8C7000227D26 /* SystemService.swift in Sources */,
30DC18522FD009CD0041DCD1 /* VipExpenseModel.swift in Sources */, 30DC18522FD009CD0041DCD1 /* VipExpenseModel.swift in Sources */,
30C4C01B2FDBF09D009215C1 /* RemoveMemberView.swift in Sources */,
307073E12FD15F50004C37CC /* GroupIMService.swift in Sources */, 307073E12FD15F50004C37CC /* GroupIMService.swift in Sources */,
305A76F22FCA8C7000227D26 /* UserService.swift in Sources */, 305A76F22FCA8C7000227D26 /* UserService.swift in Sources */,
305A76F32FCA8C7000227D26 /* AutoLayout+NSLayoutConstraint.swift in Sources */, 305A76F32FCA8C7000227D26 /* AutoLayout+NSLayoutConstraint.swift in Sources */,
@ -1575,6 +1622,7 @@
305A76FD2FCA8C7000227D26 /* EmptyDataSetSource.swift in Sources */, 305A76FD2FCA8C7000227D26 /* EmptyDataSetSource.swift in Sources */,
30EFF3CD2FDA668A00EB35D4 /* MyProfileView.swift in Sources */, 30EFF3CD2FDA668A00EB35D4 /* MyProfileView.swift in Sources */,
305A76FE2FCA8C7000227D26 /* EmptyDataSetView.swift in Sources */, 305A76FE2FCA8C7000227D26 /* EmptyDataSetView.swift in Sources */,
30C4C0192FDBF094009215C1 /* RemoveMemberVC.swift in Sources */,
305A76FF2FCA8C7000227D26 /* EmptyDataSetView+Extension.swift in Sources */, 305A76FF2FCA8C7000227D26 /* EmptyDataSetView+Extension.swift in Sources */,
305A77002FCA8C7000227D26 /* RefreshStyle.swift in Sources */, 305A77002FCA8C7000227D26 /* RefreshStyle.swift in Sources */,
305A77012FCA8C7000227D26 /* DLHUD.swift in Sources */, 305A77012FCA8C7000227D26 /* DLHUD.swift in Sources */,
@ -1643,7 +1691,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 434CGNSJ28; DEVELOPMENT_TEAM = 7LP48T8ZJE;
ENABLE_USER_SCRIPT_SANDBOXING = "$(inherited)"; ENABLE_USER_SCRIPT_SANDBOXING = "$(inherited)";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QuickLocation/Info.plist; INFOPLIST_FILE = QuickLocation/Info.plist;
@ -1664,7 +1712,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.zuom8.jisulocation; PRODUCT_BUNDLE_IDENTIFIER = cn.zuom8.jisuloca;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
@ -1692,7 +1740,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 434CGNSJ28; DEVELOPMENT_TEAM = 7LP48T8ZJE;
ENABLE_USER_SCRIPT_SANDBOXING = "$(inherited)"; ENABLE_USER_SCRIPT_SANDBOXING = "$(inherited)";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = QuickLocation/Info.plist; INFOPLIST_FILE = QuickLocation/Info.plist;
@ -1713,7 +1761,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.zuom8.jisulocation; PRODUCT_BUNDLE_IDENTIFIER = cn.zuom8.jisuloca;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -33,8 +33,8 @@ class BaseViewController: UIViewController {
/// ///
var navTitle: String = "" { var navTitle: String = "" {
didSet { didSet {
// backButton.setTitle(" \(navTitle)", for: .normal) backButton.setTitle(" \(navTitle)", for: .normal)
navigationItem.title = navTitle // navigationItem.title = navTitle
} }
} }

View File

@ -58,6 +58,10 @@ class AppContextManager: NSObject {
var sex: Int { var sex: Int {
account?.sex ?? -1 account?.sex ?? -1
} }
/// VIP
var vip: Int {
account?.vip ?? 1
}
@discardableResult @discardableResult
public func saveAccount(_ account: UserConfigModel) -> Bool { public func saveAccount(_ account: UserConfigModel) -> Bool {

View File

@ -58,6 +58,9 @@ struct UserConfigModel: Mappable {
} }
struct SystemConfigModel: Mappable { struct SystemConfigModel: Mappable {
///
var chatWarning: String = ""
/// banner /// banner
var groupBannerList: [String] = [] var groupBannerList: [String] = []
@ -66,6 +69,7 @@ struct SystemConfigModel: Mappable {
} }
mutating func mapping(map: Map) { mutating func mapping(map: Map) {
chatWarning <- map["client.chatwarning", nested: false]
groupBannerList <- map["client.team.ad", nested: false] groupBannerList <- map["client.team.ad", nested: false]
} }
} }

View File

@ -31,8 +31,14 @@ enum Route: String {
case vipRights = "vipRights" case vipRights = "vipRights"
/// ///
case groupChat = "groupChat" case groupChat = "groupChat"
///
case groupInfo = "groupInfo"
/// ///
case groupSetting = "groupSetting" case groupSetting = "groupSetting"
///
case reviewMemberList = "reviewMemberList"
///
case removeMember = "removeMember"
/// ///
case account = "account" case account = "account"
/// ///
@ -45,6 +51,8 @@ enum Route: String {
case emergencyContactAdd = "emergencyContactAdd" case emergencyContactAdd = "emergencyContactAdd"
/// ///
case privacyPolicy = "privacyPolicy" case privacyPolicy = "privacyPolicy"
///
case checkPermission = "checkPermission"
} }
extension Route: RouterTarget { extension Route: RouterTarget {
@ -181,12 +189,30 @@ extension AppRouter: AppRouterProtocol {
return GroupChatVC(groupId: groupId) return GroupChatVC(groupId: groupId)
} }
// MARK: -
AppRouter.register(Route.groupInfo) { url, parameters in
let vc = GroupInfoVC(groupInfo: parameters["groupInfo"].safeDictionary as! [String : Any])
return vc
}
// MARK: - // MARK: -
AppRouter.register(Route.groupSetting) { url, parameters in AppRouter.register(Route.groupSetting) { url, parameters in
let groupId = parameters["groupId"].safeString let groupId = parameters["groupId"].safeString
return GroupSettingVC(groupId: groupId) return GroupSettingVC(groupId: groupId)
} }
// MARK: -
AppRouter.register(Route.reviewMemberList) { url, parameters in
let groupId = parameters["groupId"].safeString
return ReviewMemberListVC(groupId: groupId)
}
// MARK: -
AppRouter.register(Route.removeMember) { url, parameters in
let groupId = parameters["groupId"].safeString
return RemoveMemberVC(groupId: groupId)
}
// MARK: - // MARK: -
AppRouter.register(Route.account) { url, parameters in AppRouter.register(Route.account) { url, parameters in
let vc = AccountVC() let vc = AccountVC()
@ -226,6 +252,11 @@ extension AppRouter: AppRouterProtocol {
AppRouter.register(Route.privacyPolicy) { url, parameters in AppRouter.register(Route.privacyPolicy) { url, parameters in
PrivacyPolicyVC() PrivacyPolicyVC()
} }
// MARK: -
AppRouter.register(Route.checkPermission) { url, parameters in
CheckPermissionVC()
}
} }
} }

View File

@ -54,6 +54,8 @@ struct GroupInfoResponse: BaseModelProtocol {
var model: GroupInfoModel? var model: GroupInfoModel?
// //
var list: [GroupMemberModel] = [] var list: [GroupMemberModel] = []
//
var reviewCount: Int = 0
init?(map: Map) {} init?(map: Map) {}
@ -62,6 +64,24 @@ struct GroupInfoResponse: BaseModelProtocol {
message <- map["msg"] message <- map["msg"]
model <- map["data.attr"] model <- map["data.attr"]
list <- map["data.employee"] list <- map["data.employee"]
reviewCount <- map["data.reviewCount"]
}
}
struct ReviewMemberListResponse: BaseModelProtocol {
//
var code: String?
//
var message: String?
///
var list: [GroupMemberModel] = []
init?(map: Map) {}
mutating func mapping(map: Map) {
code <- map["code"]
message <- map["msg"]
list <- map["data"]
} }
} }
@ -115,6 +135,8 @@ struct GroupInfoModel: Mappable, Equatable {
var description: String = "" var description: String = ""
/// ///
var level: String = "" var level: String = ""
///
var share_code: String = ""
init?(map: Map) { init?(map: Map) {
@ -129,6 +151,7 @@ struct GroupInfoModel: Mappable, Equatable {
level <- map["level"] level <- map["level"]
review <- map["review"] review <- map["review"]
description <- map["description"] description <- map["description"]
share_code <- map["share_code"]
people_no <- (map["people_no"], kStrTransformInt) people_no <- (map["people_no"], kStrTransformInt)
} }
} }

View File

@ -1,10 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict/>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:smartdrive.zuom8.cn</string>
</array>
</dict>
</plist> </plist>

View File

@ -46,16 +46,26 @@ final class GroupChatVC: BaseViewController {
setupMessageListener() setupMessageListener()
setupVoiceRecording() setupVoiceRecording()
setupPanelDismiss() setupPanelDismiss()
setupKeyboard()
// + IM SDK // + IM SDK
requestGroupInfoByKey() requestGroupInfoByKey()
viewModel.loadMessages() viewModel.loadMessages()
//
guard let config = AppContextManager.shared.systemConfig else { return }
rootView.chatWarningView.isHidden = config.chatWarning.isEmpty
rootView.chatWarningLab.text = config.chatWarning
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
setupKeyboard()
IQKeyboardManager.shared.isEnabled = false IQKeyboardManager.shared.isEnabled = false
IQKeyboardManager.shared.resignOnTouchOutside = false IQKeyboardManager.shared.resignOnTouchOutside = false
//
guard viewModel.groupId.contains(AppContextManager.shared.userId) == false else { return }
rootView.disableIMView.isHidden = AppContextManager.shared.vip > 1
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
@ -65,12 +75,19 @@ final class GroupChatVC: BaseViewController {
IQKeyboardManager.shared.resignOnTouchOutside = true IQKeyboardManager.shared.resignOnTouchOutside = true
} }
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self)
}
// MARK: - Keyboard // MARK: - Keyboard
private func setupKeyboard() { private func setupKeyboard() {
// //
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification) NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
.subscribe(onNext: { [weak self] noti in .subscribe(onNext: { [weak self] noti in
guard let self = self, guard let self = self,
// VC VC
self.isViewLoaded && self.view.window != nil,
let userInfo = noti.userInfo, let userInfo = noti.userInfo,
let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect let frame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
else { return } else { return }
@ -165,6 +182,10 @@ final class GroupChatVC: BaseViewController {
}) })
.disposed(by: disposeBag) .disposed(by: disposeBag)
rootView.reviewBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.push(Route.reviewMemberList, userInfo: ["groupId": self.viewModel.groupId])
}).disposed(by: disposeBag)
// //
rootView.voiceBtn.rx.tap.subscribe(onNext: { [weak self] _ in rootView.voiceBtn.rx.tap.subscribe(onNext: { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
@ -410,10 +431,10 @@ final class GroupChatVC: BaseViewController {
GroupService.groupInfoByKey(viewModel.groupId).subscribe { response in GroupService.groupInfoByKey(viewModel.groupId).subscribe { response in
guard let model = response.model else { return } guard let model = response.model else { return }
self.viewModel.memberList = response.list self.viewModel.memberList = response.list
self.viewModel.buildAvatarCache()
self.rootView.groupNameLabel.text = model.name self.rootView.groupNameLabel.text = model.name
self.rootView.groupAvatarView.image = model.groupIcon self.rootView.groupAvatarView.image = model.groupIcon
self.rootView.reviewBtn.isHidden = !model.is_owner self.rootView.reviewBtn.isHidden = !model.is_owner
self.rootView.reviewDotView.isHidden = response.reviewCount == 0
}.disposed(by: disposeBag) }.disposed(by: disposeBag)
} }
@ -517,6 +538,7 @@ extension GroupChatVC: PhotoPickerControllerDelegate {
let localMsg = ChatMessage( let localMsg = ChatMessage(
id: localId, id: localId,
isSelf: true, isSelf: true,
senderId: AppContextManager.shared.userId,
avatar: viewModel.getUserAvatar(id: AppContextManager.shared.userId), avatar: viewModel.getUserAvatar(id: AppContextManager.shared.userId),
senderName: AppContextManager.shared.name, senderName: AppContextManager.shared.name,
content: "", content: "",

View File

@ -21,6 +21,7 @@ enum VoiceRecordState {
struct ChatMessage { struct ChatMessage {
let id: String let id: String
let isSelf: Bool let isSelf: Bool
let senderId: String
let avatar: UIImage let avatar: UIImage
let senderName: String let senderName: String
let content: String let content: String
@ -78,9 +79,12 @@ class GroupChatView: UIView {
rightIconsView.addSubview(reviewBtn) rightIconsView.addSubview(reviewBtn)
rightIconsView.addSubview(memberBtn) rightIconsView.addSubview(memberBtn)
rightIconsView.addSubview(settingBtn) rightIconsView.addSubview(settingBtn)
rightIconsView.addSubview(reviewDotView)
addSubview(tableView) addSubview(tableView)
addSubview(chatWarningView)
addSubview(bottomBar) addSubview(bottomBar)
addSubview(disableIMView)
bottomBar.addSubview(bottomBarCornerView) bottomBar.addSubview(bottomBarCornerView)
addSubview(voiceRecordView) addSubview(voiceRecordView)
addSubview(emojiPanelView) addSubview(emojiPanelView)
@ -127,6 +131,12 @@ class GroupChatView: UIView {
.left().centerY() .left().centerY()
.width(24).height(24) .width(24).height(24)
reviewDotView.layoutChain
.topToView(reviewBtn, offset: -2)
.leftToRightOfView(reviewBtn, offset: -6)
.width(8)
.height(8)
memberBtn.layoutChain memberBtn.layoutChain
.leftToRightOfView(reviewBtn, offset: 20) .leftToRightOfView(reviewBtn, offset: 20)
.centerY() .centerY()
@ -143,11 +153,20 @@ class GroupChatView: UIView {
.edgesHorzontal() .edgesHorzontal()
.bottomToTopOfView(bottomBar) .bottomToTopOfView(bottomBar)
chatWarningView.layoutChain
.topToBottomOfView(navBarView)
.edgesHorzontal()
bottomBar.layoutChain bottomBar.layoutChain
.edgesHorzontal(15) .edgesHorzontal(15)
.height(50) .height(50)
.bottom(kSafeBottomMargin + 20) .bottom(kSafeBottomMargin + 20)
disableIMView.layoutChain
.edgesHorzontal(15)
.height(50)
.bottom(kSafeBottomMargin + 20)
bottomBarBottomConstraint = bottomBar.jh_constraint(.bottom, toAttribute: .bottom, otherView: bottomBar.superview, relation: .equal) bottomBarBottomConstraint = bottomBar.jh_constraint(.bottom, toAttribute: .bottom, otherView: bottomBar.superview, relation: .equal)
bottomBarCornerView.layoutChain.edges() bottomBarCornerView.layoutChain.edges()
@ -254,6 +273,14 @@ class GroupChatView: UIView {
return btn return btn
}() }()
lazy var reviewDotView: UIView = {
let view = UIView()
view.backgroundColor = .red
view.cornerRadius = 4
view.isHidden = true
return view
}()
lazy var memberBtn: UIButton = { lazy var memberBtn: UIButton = {
let btn = UIButton(type: .custom) let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "IM/member"), for: .normal) btn.setImage(UIImage(named: "IM/member"), for: .normal)
@ -266,6 +293,47 @@ class GroupChatView: UIView {
return btn return btn
}() }()
// MARK: - chatwarning
lazy var chatWarningView: UIView = {
let view = UIView()
view.backgroundColor = .black.withAlphaComponent(0.5)
view.isHidden = true
view.addSubview(closeBtn)
closeBtn.layoutChain
.right(10)
.height(10)
.width(10)
.centerY()
view.addSubview(chatWarningLab)
chatWarningLab.layoutChain
.edgesVertical(5)
.left(15)
.rightToLeftOfView(closeBtn, offset: -8)
return view
}()
lazy var closeBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.backgroundColor = .clear
btn.setImage(UIImage(named: "Group/close"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 20, left: 30, bottom: 20, right: 10)
btn.rx.tap.subscribe(onNext: { _ in
self.chatWarningView.isHidden = true
}).disposed(by: disposeBag)
return btn
}()
lazy var chatWarningLab: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = .systemFont(ofSize: 10, weight: .regular)
label.numberOfLines = 0
return label
}()
// MARK: - Message List // MARK: - Message List
lazy var tableView: UITableView = { lazy var tableView: UITableView = {
let tv = UITableView(frame: .zero, style: .plain) let tv = UITableView(frame: .zero, style: .plain)
@ -346,6 +414,32 @@ class GroupChatView: UIView {
return btn return btn
}() }()
/// IM
lazy var disableIMView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.1).cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 0)
view.layer.shadowOpacity = 1
view.layer.shadowRadius = 9
view.isHidden = true
let v = UIView()
v.backgroundColor = .white
v.cornerRadius = 25
view.addSubview(v)
v.layoutChain.edges()
let label = UILabel()
label.text = "开通VIP后可发送消息"
label.textColor = ThemeManager.shared.color.titleAuxColor
label.font = .systemFont(ofSize: 15, weight: .medium)
view.addSubview(label)
label.layoutChain.centerX().centerY()
return view
}()
/// ///
lazy var voiceRecordView: VoiceRecordView = { lazy var voiceRecordView: VoiceRecordView = {
let v = VoiceRecordView() let v = VoiceRecordView()

View File

@ -45,7 +45,11 @@ final class GroupChatViewModel {
private let timeGapThreshold: TimeInterval = 300 // 5 minutes private let timeGapThreshold: TimeInterval = 300 // 5 minutes
var groupModel: GroupInfoModel? var groupModel: GroupInfoModel?
var memberList: [GroupMemberModel] = [] var memberList: [GroupMemberModel] = [] {
didSet {
buildAvatarCache()
}
}
var groupId: String = "" var groupId: String = ""
// MARK: - Init // MARK: - Init
@ -72,6 +76,43 @@ final class GroupChatViewModel {
cache[member.user_id] = member.userIcon cache[member.user_id] = member.userIcon
} }
avatarCache = cache avatarCache = cache
// loadMessages memberList
refreshMessageAvatars()
}
/// avatarCache loadMessages memberList
private func refreshMessageAvatars() {
var items = messagesSubject.value
var didChange = false
items = items.map { item in
switch item {
case var .send(m): if updateAvatar(&m) { didChange = true }; return .send(m)
case var .received(m): if updateAvatar(&m) { didChange = true }; return .received(m)
case var .emojiSend(m): if updateAvatar(&m) { didChange = true }; return .emojiSend(m)
case var .emojiReceived(m): if updateAvatar(&m) { didChange = true }; return .emojiReceived(m)
case var .voiceSend(m): if updateAvatar(&m) { didChange = true }; return .voiceSend(m)
case var .voiceReceived(m): if updateAvatar(&m) { didChange = true }; return .voiceReceived(m)
case var .imageSend(m): if updateAvatar(&m) { didChange = true }; return .imageSend(m)
case var .imageReceived(m): if updateAvatar(&m) { didChange = true }; return .imageReceived(m)
default: return item
}
}
if didChange {
messagesSubject.accept(items)
}
}
///
private func updateAvatar(_ msg: inout ChatMessage) -> Bool {
guard let cached = avatarCache[msg.senderId], cached != msg.avatar else { return false }
msg = ChatMessage(
id: msg.id, isSelf: msg.isSelf, senderId: msg.senderId,
avatar: cached, senderName: msg.senderName,
content: msg.content, voiceUrl: msg.voiceUrl, imageUrl: msg.imageUrl,
imageWidth: msg.imageWidth, imageHeight: msg.imageHeight,
timestamp: msg.timestamp, showTime: msg.showTime, isUploading: msg.isUploading
)
return true
} }
func getUserAvatar(id: String) -> UIImage { func getUserAvatar(id: String) -> UIImage {
@ -81,7 +122,7 @@ final class GroupChatViewModel {
avatarCache[id] = image avatarCache[id] = image
return image return image
} }
return UIImage(named: "GroupIcon1") ?? UIImage() return UIImage(named: "UserIcon/1") ?? UIImage()
} }
func getUserNickName(id: String) -> String { func getUserNickName(id: String) -> String {
@ -141,6 +182,8 @@ final class GroupChatViewModel {
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.messagesSubject.accept(items) self.messagesSubject.accept(items)
//
self.markAsRead()
} }
}, },
onFailure: { code, msg in onFailure: { code, msg in
@ -148,6 +191,14 @@ final class GroupChatViewModel {
}) })
} }
///
private func markAsRead() {
let conversationID = "sg_\(groupId)"
OIMManager.manager.markConversationMessage(asRead: conversationID,
onSuccess: nil,
onFailure: nil)
}
/// ///
func appendLocalMessage(_ item: ChatSectionItem) { func appendLocalMessage(_ item: ChatSectionItem) {
var items = messagesSubject.value var items = messagesSubject.value
@ -272,10 +323,12 @@ final class GroupChatViewModel {
imageW = 0; imageH = 0 imageW = 0; imageH = 0
} }
let sendID = msg.sendID ?? ""
return ChatMessage( return ChatMessage(
id: msg.clientMsgID ?? UUID().uuidString, id: msg.clientMsgID ?? UUID().uuidString,
isSelf: isSelf, isSelf: isSelf,
avatar: getUserAvatar(id: msg.sendID ?? ""), senderId: sendID,
avatar: getUserAvatar(id: sendID),
senderName: msg.senderNickname ?? "", senderName: msg.senderNickname ?? "",
content: content, content: content,
voiceUrl: voiceUrl, voiceUrl: voiceUrl,
@ -322,19 +375,27 @@ final class GroupChatViewModel {
private func parseNotification(_ elem: OIMNotificationElem, contentType: Int) -> NSAttributedString? { private func parseNotification(_ elem: OIMNotificationElem, contentType: Int) -> NSAttributedString? {
guard let data = elem.detail?.data(using: .utf8), guard let data = elem.detail?.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let group = json["group"] as? [String: Any], let group = json["group"] as? [String: Any] else { return nil }
let opUser = json["opUser"] as? [String: Any] else { return nil }
switch contentType { switch contentType {
case 1501, 1510: case 1501:
// // admin
let ownerID = group["ownerUserID"] as? String ?? "" guard let opUser = json["opUser"] as? [String: Any] else { return nil }
let ownerNickName = getUserNickName(id: ownerID) let groupID = opUser["groupID"] as? String ?? ""
let text = ownerID == AppContextManager.shared.userId ? "圈子已经创建" : "\(ownerNickName) 创建了圈子" let isOwner = groupID.contains(AppContextManager.shared.userId)
let text = isOwner ? "\(AppContextManager.shared.name) 创建了圈子" : "圈子已经创建"
return NSAttributedString(string: text)
case 1510:
//
guard let entrantUser = json["entrantUser"] as? [String: Any] else { return nil }
let nickName = entrantUser["nickname"] as? String ?? entrantUser["userID"] as? String ?? ""
let text = "\(nickName) 加入了圈子"
return NSAttributedString(string: text) return NSAttributedString(string: text)
case 1520: case 1520:
// //
guard let opUser = json["opUser"] as? [String: Any] else { return nil }
let opUserID = opUser["userID"] as? String ?? "" let opUserID = opUser["userID"] as? String ?? ""
let opNickName = getUserNickName(id: opUserID) let opNickName = getUserNickName(id: opUserID)
let newName = group["groupName"] as? String ?? "" let newName = group["groupName"] as? String ?? ""
@ -343,7 +404,6 @@ final class GroupChatViewModel {
let tip = "\(opNickName) 将群名称修改为 " let tip = "\(opNickName) 将群名称修改为 "
let result = NSMutableAttributedString(string: tip + newName) let result = NSMutableAttributedString(string: tip + newName)
result.addAttribute(.font, value: UIFont.systemFont(ofSize: 12), range: NSRange(location: 0, length: result.length)) result.addAttribute(.font, value: UIFont.systemFont(ofSize: 12), range: NSRange(location: 0, length: result.length))
//
let nameRange = NSRange(location: tip.count, length: newName.utf16.count) let nameRange = NSRange(location: tip.count, length: newName.utf16.count)
result.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: nameRange) result.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: nameRange)
return result return result

View File

@ -0,0 +1,59 @@
//
// GroupInfoVC.swift
// QuickLocation
//
// Created by on 2026/6/12.
//
import UIKit
import ObjectMapper
import RxSwift
import RxCocoa
class GroupInfoVC: BaseViewController {
fileprivate var rootView: GroupInfoView!
override func loadView() {
rootView = GroupInfoView(frame: UIScreen.main.bounds)
view = rootView
}
private let groupInModel: GroupInfoModel?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
guard let model = groupInModel else { return }
rootView.groupIcon.image = model.groupIcon
rootView.groupNameLab.text = model.name
rootView.applyBtn.rx.tap.subscribe(onNext: { _ in
self.requestOperateGroup()
}).disposed(by: disposeBag)
}
// MARK: - API
private func requestOperateGroup() {
guard let model = groupInModel else { return }
DLToast.showLoading()
GroupService.operate(opType: "join",
requestData: ["share_code" : model.share_code]).subscribe(onNext: { response in
DLToast.show(text: "申请成功")
}, onError: { (error) in }).disposed(by: disposeBag)
}
// MARK: - Init
init(groupInfo: [String: Any]) {
self.groupInModel = GroupInfoModel.init(JSON: groupInfo)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,131 @@
//
// GroupInfoView.swift
// QuickLocation
//
// Created by on 2026/6/12.
//
import UIKit
import RxSwift
import RxCocoa
class GroupInfoView: UIView {
var disposeBag = DisposeBag()
private func setupRx() {
backBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.shared.popOrDismiss()
}).disposed(by: disposeBag)
}
private func setupUI() {
addSubview(navBgView)
addSubview(navBarView)
navBarView.addSubview(navTitleLabel)
navBarView.addSubview(backBtn)
addSubview(groupIcon)
addSubview(groupNameLab)
addSubview(applyBtn)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.heightToWidth(160/375)
navBarView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navTitleLabel.layoutChain
.top(kStatusBarHeight + 12)
.centerY(backBtn)
.centerX()
backBtn.layoutChain
.centerY(navTitleLabel)
.left(15)
.width(24)
.height(24)
groupIcon.layoutChain
.topToBottomOfView(navBarView, offset: 24)
.width(80)
.height(80)
.centerX()
groupNameLab.layoutChain
.topToBottomOfView(groupIcon, offset: 4)
.centerX()
applyBtn.layoutChain
.edgesHorzontal(30)
.bottom(kSafeBottomMargin + 10)
.height(50)
}
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill
return iv
}()
lazy var navBarView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var navTitleLabel: UILabel = {
let label = UILabel()
label.text = "圈子资料"
label.font = .systemFont(ofSize: 18, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.textAlignment = .center
return label
}()
lazy var backBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100)
return btn
}()
lazy var groupIcon: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.cornerRadius = 40
return view
}()
lazy var groupNameLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
return label
}()
lazy var applyBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("申请加入", for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setTitleColor(.white, for: .normal)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 25
return btn
}()
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .white
setupUI()
setupRx()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -85,10 +85,22 @@ class GroupSettingVC: BaseViewController {
AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": model.toJSON()]) AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": model.toJSON()])
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
//
rootView.auditMemberView.rx.tapGesture.subscribe(onNext: { _ in
guard let model = self.viewModel.groupModel else { return }
AppRouter.push(Route.reviewMemberList, userInfo: ["groupId": model.group_key])
}).disposed(by: disposeBag)
//
rootView.removeMemberView.rx.tapGesture.subscribe(onNext: { _ in
guard let model = self.viewModel.groupModel else { return }
AppRouter.push(Route.removeMember, userInfo: ["groupId": model.group_key])
}).disposed(by: disposeBag)
// //
rootView.dismissGroupView.rx.tapGesture.subscribe(onNext: { _ in rootView.dismissGroupView.rx.tapGesture.subscribe(onNext: { _ in
VerificationPopView.show { VerificationPopView.show {
self.requestDismissGroup()
} }
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
@ -165,6 +177,16 @@ class GroupSettingVC: BaseViewController {
}.disposed(by: disposeBag) }.disposed(by: disposeBag)
} }
private func requestDismissGroup() {
DLToast.showLoading()
GroupService.dismiss(requestData: ["group_key": viewModel.groupId]).subscribe { response in
DLToast.dismiss()
DLToast.show(text: "成功解散") {
AppRouter.shared.popToRoot()
}
}.disposed(by: disposeBag)
}
// MARK: - Init // MARK: - Init
init(groupId: String) { init(groupId: String) {
self.viewModel = GroupSettingViewModel(groupId: groupId) self.viewModel = GroupSettingViewModel(groupId: groupId)

View File

@ -54,11 +54,10 @@ final class GroupViewController: BaseViewController {
guard !hasSetupIMListeners else { return } guard !hasSetupIMListeners else { return }
hasSetupIMListeners = true hasSetupIMListeners = true
GroupIMService.shared.setConversationListener { [weak self] _ in // 使 MJExtension
GroupIMService.shared.getConversationList { conversations in GroupIMService.shared.setConversationListener { [weak self] conversations in
self?.viewModel.updateConversations(conversations) self?.viewModel.updateConversations(conversations)
} }
}
// /SDK // /SDK
GroupIMService.shared.setGroupListener { [weak self] in GroupIMService.shared.setGroupListener { [weak self] in
@ -105,6 +104,10 @@ final class GroupViewController: BaseViewController {
.observe(on: MainScheduler.asyncInstance) .observe(on: MainScheduler.asyncInstance)
.bind(to: rootView.joinedTableView.rx.items(dataSource: joinedDataSource)) .bind(to: rootView.joinedTableView.rx.items(dataSource: joinedDataSource))
.disposed(by: disposeBag) .disposed(by: disposeBag)
rootView.hotGroupsCollectionView.rx.modelSelected(GroupInfoModel.self)
.subscribe(viewModel.hotGroupCellAction.inputs)
.disposed(by: disposeBag)
} }
private func reactiveAction() { private func reactiveAction() {

View File

@ -8,6 +8,7 @@ import RxSwift
import RxCocoa import RxCocoa
import RxDataSources import RxDataSources
import OpenIMSDK import OpenIMSDK
import ObjectMapper
typealias HotGroupListSectionModel = SectionModel<String, GroupInfoModel> typealias HotGroupListSectionModel = SectionModel<String, GroupInfoModel>
typealias CircleListSectionModel = SectionModel<String, GroupCellData> typealias CircleListSectionModel = SectionModel<String, GroupCellData>
@ -52,6 +53,13 @@ final class GroupViewModel {
hotGroupSectionedItems.onNext(list.mapSection()) hotGroupSectionedItems.onNext(list.mapSection())
} }
lazy var hotGroupCellAction: Action<GroupInfoModel, Void> = { this in
return Action { model in
AppRouter.push(Route.groupInfo, userInfo: ["groupInfo": model.toJSON()])
return .empty()
}
}(self)
/// ///
func loadIMGroups(_ groups: [OIMGroupInfo], conversations: [OIMConversationInfo] = []) { func loadIMGroups(_ groups: [OIMGroupInfo], conversations: [OIMConversationInfo] = []) {
if !groups.isEmpty { cachedGroups = groups } if !groups.isEmpty { cachedGroups = groups }

View File

@ -0,0 +1,118 @@
//
// RemoveMemberVC.swift
// QuickLocation
//
// Created by on 2026/6/12.
//
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
import ObjectMapper
class RemoveMemberVC: BaseViewController {
fileprivate var rootView: RemoveMemberView!
override func loadView() {
rootView = RemoveMemberView(frame: UIScreen.main.bounds)
view = rootView
}
private var viewModel: RemoveMemberViewModel
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
bindViewModel()
requestGroupInfo()
}
private func bindViewModel() {
viewModel.output.sectionedItems
.bind(to: rootView.tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
viewModel.output.sectionedItems
.subscribe(onNext: { [weak self] sections in
let count = sections.first?.items.count ?? 0
self?.rootView.updateHeight(itemCount: count)
})
.disposed(by: disposeBag)
rootView.tableView.rx.modelSelected(GroupMemberModel.self)
.subscribe(viewModel.cellAction.inputs)
.disposed(by: disposeBag)
// selectedMembers
let selectedCount = viewModel.selectedMembers
.map { $0.count }
.share(replay: 1)
selectedCount
.map { $0 > 0 }
.bind(to: rootView.deleteBtn.rx.isEnabled)
.disposed(by: disposeBag)
selectedCount
.map { "删除(\($0))" }
.bind(to: rootView.deleteBtn.rx.title())
.disposed(by: disposeBag)
rootView.deleteBtn.rx.tap.subscribe(onNext: { _ in
self.showConfirmPop(showCloseBtn: true,
title: "确定要移除成员吗?",
message: "成员将从当前圈子中移除",
confirmText: "",
cancelText: "") {
self.requestRemoveMember()
}
}).disposed(by: disposeBag)
}
// MARK: - UITableViewDataSource
lazy private var dataSource: RxTableViewSectionedReloadDataSource<ReviewMemberListSectionModel> = {
return RxTableViewSectionedReloadDataSource<ReviewMemberListSectionModel>(
configureCell: { (_, tableView, indexPath, model) in
let cell: RemoveMemberCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model,
isOwn: self.viewModel.groupId.contains(model.user_id),
isSelected: self.viewModel.isSelected(id: model.user_id))
return cell
})
}()
// MARK: - API
private func requestGroupInfo() {
DLToast.showLoading()
GroupService.groupInfoByKey(viewModel.groupId).subscribe { response in
DLToast.dismiss()
guard let model = response.model else { return }
self.viewModel.groupModel = model
self.viewModel.loadData(response.list)
}.disposed(by: disposeBag)
}
// MARK: - API
private func requestRemoveMember() {
DLToast.showLoading()
GroupService.kickMember(requestData: ["group_key": viewModel.groupId,
"kick_userid": viewModel.selectedMembers.value]).subscribe { response in
DLToast.dismiss()
self.viewModel.selectedMembers.accept([])
self.requestGroupInfo()
}.disposed(by: disposeBag)
}
// MARK: - Init
init(groupId: String) {
self.viewModel = RemoveMemberViewModel(groupId: groupId)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,304 @@
//
// RemoveMemberView.swift
// QuickLocation
//
// Created by on 2026/6/12.
//
import UIKit
import RxSwift
import RxCocoa
class RemoveMemberView: UIView {
var disposeBag = DisposeBag()
///
static let rowHeight: CGFloat = 85
private func setupRx() {
backBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.shared.popOrDismiss()
}).disposed(by: disposeBag)
}
private func setupUI() {
addSubview(navBgView)
addSubview(navBarView)
navBarView.addSubview(navTitleLabel)
navBarView.addSubview(backBtn)
navBarView.addSubview(deleteBtn)
addSubview(titleLab)
addSubview(cornerBgView)
addSubview(tableView)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navBarView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navTitleLabel.layoutChain
.top(kStatusBarHeight + 12)
.centerX()
backBtn.layoutChain
.centerY(navTitleLabel)
.left(15)
.width(24)
.height(24)
deleteBtn.layoutChain
.right(15)
.width(100)
.height(44)
.centerY(navTitleLabel)
titleLab.layoutChain
.topToBottomOfView(navBarView, offset: 8)
.left(15)
cornerBgView.layoutChain
.topToBottomOfView(titleLab, offset: 16)
.edgesHorzontal(15)
tableView.layoutChain
.topToBottomOfView(titleLab, offset: 16)
.edgesHorzontal(15)
}
/// bottom = kSafeBottomMargin + 10
func updateHeight(itemCount: Int) {
let contentH = CGFloat(itemCount) * Self.rowHeight
let maxH = dl.height - kNaviHeight - kSafeBottomMargin - 10 - 16
let targetH = min(contentH, maxH)
tableView.layoutChain.height(targetH)
cornerBgView.layoutChain.height(targetH)
tableView.isScrollEnabled = targetH > maxH
}
private var listHeightConstraint: NSLayoutConstraint?
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill
return iv
}()
lazy var navBarView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var navTitleLabel: UILabel = {
let label = UILabel()
label.text = "移除圈子成员"
label.font = .systemFont(ofSize: 18, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.textAlignment = .center
return label
}()
lazy var backBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100)
return btn
}()
lazy var deleteBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("删除(0)", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#FF383C"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
btn.contentHorizontalAlignment = .right
return btn
}()
lazy var titleLab: UILabel = {
let label = UILabel()
label.text = "圈子成员"
label.font = .systemFont(ofSize: 16, weight: .semibold)
label.textColor = ThemeManager.shared.color.titleAuxColor
return label
}()
lazy var cornerBgView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#F5FBFF")
view.cornerRadius = 10
return view
}()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.estimatedRowHeight = 85
tableView.isScrollEnabled = false
tableView.register(RemoveMemberCell.self)
return tableView
}()
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .white
setupUI()
setupRx()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - RemoveMemberCell
class RemoveMemberCell: UITableViewCell {
var disposeBag = DisposeBag()
func configure(_ model: GroupMemberModel, isOwn: Bool, isSelected: Bool) {
avaterImgView.image = model.userIcon
nameLab.text = model.nick_name
ownView.isHidden = !isOwn
selectedBtn.isHidden = isOwn
selectedBtn.isSelected = isSelected
}
override init(style: CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
setupSubviews()
}
private func setupSubviews() {
contentView.addSubview(bgView)
bgView.addSubview(avaterImgView)
bgView.addSubview(nameLab)
bgView.addSubview(stackView)
bgView.addSubview(lineView)
bgView.layoutChain
.edgesHorzontal()
.edgesVertical()
avaterImgView.layoutChain
.edgesVertical(20)
.left(15)
.height(45)
.widthToHeight(1)
nameLab.layoutChain
.leftToRightOfView(avaterImgView, offset: 10)
.centerY(avaterImgView)
stackView.layoutChain
.right(15)
.centerY()
ownView.layoutChain.width(25).height(35)
selectedBtn.layoutChain.width(20).height(20)
lineView.layoutChain
.height(0.5)
.edgesHorzontal(15)
.bottom()
}
lazy var bgView: UIView = {
let view = UIView()
view.backgroundColor = .clear//UIColor(hexStr: "#F5FBFF")
return view
}()
lazy var avaterImgView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.cornerRadius = 22.5
return view
}()
lazy var nameLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#0F2846")
label.font = .systemFont(ofSize: 14, weight: .medium)
return label
}()
lazy var stackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [ownView, selectedBtn])
view.axis = .horizontal
view.alignment = .trailing
view.spacing = 0
view.backgroundColor = .clear
return view
}()
lazy var ownView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.isHidden = true
let icon = UIImageView(image: UIImage(named: "Common/owner"))
view.addSubview(icon)
icon.layoutChain
.top()
.centerX()
.width(18)
.height(18)
let label = UILabel()
label.text = "圈主"
label.textColor = ThemeManager.shared.color.titleAuxColor
label.font = .systemFont(ofSize: 10, weight: .medium)
view.addSubview(label)
label.layoutChain
.topToBottomOfView(icon)
.centerX()
return view
}()
lazy var selectedBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/checkbox_20x20"), for: .normal)
btn.setImage(UIImage(named: "Common/delete_check"), for: .selected)
btn.isUserInteractionEnabled = false
btn.isHidden = true
return btn
}()
lazy var lineView: UIView = {
let view = UIView()
view.backgroundColor = ThemeManager.shared.color.lineColor
return view
}()
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
}

View File

@ -0,0 +1,65 @@
//
// RemoveMemberViewModel.swift
// QuickLocation
//
// Created by on 2026/6/12.
//
import RxSwift
import RxCocoa
import RxDataSources
import ObjectMapper
typealias RemoveMemberListSectionModel = SectionModel<String, GroupMemberModel>
class RemoveMemberViewModel {
let groupId: String
var groupModel: GroupInfoModel?
struct Output {
var sectionedItems: Observable<[ReviewMemberListSectionModel]>
}
let output: Output
private var disposeBag = DisposeBag()
private let sectionedItems = PublishSubject<[ReviewMemberListSectionModel]>()
private var list: [GroupMemberModel] = []
var selectedMembers = BehaviorRelay<[String]>(value: [])
lazy var cellAction: Action<GroupMemberModel, Void> = { this in
return Action { model in
let memberId = model.user_id
guard !self.groupId.contains(memberId) else { return .empty() }
var current = this.selectedMembers.value
if let idx = current.firstIndex(of: memberId) {
current.remove(at: idx)
} else {
current.append(memberId)
}
this.selectedMembers.accept(current)
self.sectionedItems.onNext(self.list.mapSection())
return .empty()
}
}(self)
func isSelected(id: String) -> Bool {
(self.selectedMembers.value.first(where: { $0 == id }) != nil)
}
func loadData(_ list: [GroupMemberModel]) {
self.list = list
sectionedItems.onNext(list.mapSection())
}
init(groupId: String) {
self.groupId = groupId
output = Output(
sectionedItems: sectionedItems.asObservable()
)
}
}

View File

@ -35,6 +35,13 @@ class ReviewMemberListVC: BaseViewController {
viewModel.output.sectionedItems viewModel.output.sectionedItems
.bind(to: rootView.tableView.rx.items(dataSource: dataSource)) .bind(to: rootView.tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag) .disposed(by: disposeBag)
viewModel.output.sectionedItems
.subscribe(onNext: { [weak self] sections in
let count = sections.first?.items.count ?? 0
self?.rootView.updateHeight(itemCount: count)
})
.disposed(by: disposeBag)
} }
// MARK: - UITableViewDataSource // MARK: - UITableViewDataSource
@ -43,6 +50,15 @@ class ReviewMemberListVC: BaseViewController {
configureCell: { (_, tableView, indexPath, model) in configureCell: { (_, tableView, indexPath, model) in
let cell: ReviewMemberCell = tableView.dequeueReusableCell(for: indexPath) let cell: ReviewMemberCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(model) cell.configure(model)
cell.agreeBtn.rx.tap.subscribe(onNext: { _ in
self.requestReviewMember(type: 1, userId: model.user_id)
}).disposed(by: cell.disposeBag)
cell.refuseBtn.rx.tap.subscribe(onNext: { _ in
self.requestReviewMember(type: 2, userId: model.user_id)
}).disposed(by: cell.disposeBag)
return cell return cell
}) })
}() }()
@ -52,7 +68,17 @@ class ReviewMemberListVC: BaseViewController {
DLToast.showLoading() DLToast.showLoading()
GroupService.reviewlist(requestData: ["group_key": viewModel.groupId]).subscribe { response in GroupService.reviewlist(requestData: ["group_key": viewModel.groupId]).subscribe { response in
DLToast.dismiss() DLToast.dismiss()
self.viewModel.loadData(response.list)
}.disposed(by: disposeBag)
}
private func requestReviewMember(type: Int, userId: String) {
DLToast.showLoading()
GroupService.reviewMember(requestData: ["group_key": viewModel.groupId,
"review_op": type,
"review_userid": userId]).subscribe { response in
DLToast.dismiss()
self.requestReviewlist()
}.disposed(by: disposeBag) }.disposed(by: disposeBag)
} }

View File

@ -26,6 +26,10 @@ struct ReviewMemberListViewModel {
private var disposeBag = DisposeBag() private var disposeBag = DisposeBag()
private let sectionedItems = PublishSubject<[ReviewMemberListSectionModel]>() private let sectionedItems = PublishSubject<[ReviewMemberListSectionModel]>()
func loadData(_ list: [GroupMemberModel]) {
sectionedItems.onNext(list.mapSection())
}
init(groupId: String) { init(groupId: String) {
self.groupId = groupId self.groupId = groupId
output = Output( output = Output(

View File

@ -12,6 +12,8 @@ import RxCocoa
class ReviewMemberListView: UIView { class ReviewMemberListView: UIView {
var disposeBag = DisposeBag() var disposeBag = DisposeBag()
///
static let rowHeight: CGFloat = 85
private func setupRx() { private func setupRx() {
backBtn.rx.tap.subscribe(onNext: { _ in backBtn.rx.tap.subscribe(onNext: { _ in
@ -30,7 +32,7 @@ class ReviewMemberListView: UIView {
navBgView.layoutChain navBgView.layoutChain
.edges(excludingEdge: .bottom) .edges(excludingEdge: .bottom)
.heightToWidth(160/375) .height(kNaviHeight)
navBarView.layoutChain navBarView.layoutChain
.edges(excludingEdge: .bottom) .edges(excludingEdge: .bottom)
@ -38,7 +40,6 @@ class ReviewMemberListView: UIView {
navTitleLabel.layoutChain navTitleLabel.layoutChain
.top(kStatusBarHeight + 12) .top(kStatusBarHeight + 12)
.centerY(backBtn)
.centerX() .centerX()
backBtn.layoutChain backBtn.layoutChain
@ -48,14 +49,27 @@ class ReviewMemberListView: UIView {
.height(24) .height(24)
cornerBgView.layoutChain cornerBgView.layoutChain
.edges(excludingEdge: .bottom) .topToBottomOfView(navBarView, offset: 16)
.bottom(kSafeBottomMargin + 10, relation: .greaterThanOrEqual) .edgesHorzontal(15)
tableView.layoutChain tableView.layoutChain
.edges(excludingEdge: .bottom) .topToBottomOfView(navBarView, offset: 16)
.bottom(kSafeBottomMargin + 10, relation: .greaterThanOrEqual) .edgesHorzontal(15)
} }
/// bottom = kSafeBottomMargin + 10
func updateHeight(itemCount: Int) {
let contentH = CGFloat(itemCount) * Self.rowHeight
let maxH = dl.height - kNaviHeight - kSafeBottomMargin - 10 - 16
let targetH = min(contentH, maxH)
tableView.layoutChain.height(targetH)
cornerBgView.layoutChain.height(targetH)
tableView.isScrollEnabled = targetH > maxH
}
private var listHeightConstraint: NSLayoutConstraint?
lazy var navBgView: UIImageView = { lazy var navBgView: UIImageView = {
let iv = UIImageView() let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2") iv.image = UIImage(named: "Common/navBar_bg_2")
@ -96,7 +110,8 @@ class ReviewMemberListView: UIView {
let tableView = UITableView(frame: .zero, style: .plain) let tableView = UITableView(frame: .zero, style: .plain)
tableView.backgroundColor = .clear tableView.backgroundColor = .clear
tableView.separatorStyle = .none tableView.separatorStyle = .none
tableView.estimatedRowHeight = 77 tableView.estimatedRowHeight = 85
tableView.isScrollEnabled = false
tableView.register(ReviewMemberCell.self) tableView.register(ReviewMemberCell.self)
return tableView return tableView
}() }()
@ -118,8 +133,11 @@ class ReviewMemberListView: UIView {
// MARK: - ReviewMemberCell // MARK: - ReviewMemberCell
class ReviewMemberCell: UITableViewCell { class ReviewMemberCell: UITableViewCell {
func configure(_ model: GroupMemberModel) { var disposeBag = DisposeBag()
func configure(_ model: GroupMemberModel) {
avaterImgView.image = model.userIcon
nameLab.text = model.nick_name
} }
override init(style: CellStyle, reuseIdentifier: String?) { override init(style: CellStyle, reuseIdentifier: String?) {
@ -135,6 +153,7 @@ class ReviewMemberCell: UITableViewCell {
bgView.addSubview(nameLab) bgView.addSubview(nameLab)
bgView.addSubview(agreeBtn) bgView.addSubview(agreeBtn)
bgView.addSubview(refuseBtn) bgView.addSubview(refuseBtn)
bgView.addSubview(lineView)
bgView.layoutChain bgView.layoutChain
.edgesHorzontal(15) .edgesHorzontal(15)
@ -152,15 +171,20 @@ class ReviewMemberCell: UITableViewCell {
agreeBtn.layoutChain agreeBtn.layoutChain
.centerY() .centerY()
.right(15) .right(5)
.width(34) .width(50)
.height(15) .height(30)
refuseBtn.layoutChain refuseBtn.layoutChain
.centerY() .centerY()
.rightToLeftOfView(agreeBtn, offset: 8) .rightToLeftOfView(agreeBtn, offset: -8)
.width(34) .width(50)
.height(15) .height(30)
lineView.layoutChain
.height(0.5)
.edgesHorzontal(15)
.bottom()
} }
lazy var bgView: UIView = { lazy var bgView: UIView = {
@ -187,9 +211,9 @@ class ReviewMemberCell: UITableViewCell {
let btn = UIButton(type: .custom) let btn = UIButton(type: .custom)
btn.setTitle("同意", for: .normal) btn.setTitle("同意", for: .normal)
btn.setTitleColor(.white, for: .normal) btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 10, weight: .regular) btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .regular)
btn.setBackgroundColor(UIColor(hexStr: "#16B3FF"), for: .normal) btn.setBackgroundColor(UIColor(hexStr: "#16B3FF"), for: .normal)
btn.cornerRadius = 7.5 btn.cornerRadius = 15
return btn return btn
}() }()
@ -197,14 +221,20 @@ class ReviewMemberCell: UITableViewCell {
let btn = UIButton(type: .custom) let btn = UIButton(type: .custom)
btn.setTitle("拒绝", for: .normal) btn.setTitle("拒绝", for: .normal)
btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal) btn.setTitleColor(UIColor(hexStr: "#16B3FF"), for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 10, weight: .regular) btn.titleLabel?.font = .systemFont(ofSize: 14, weight: .regular)
btn.setBackgroundColor(.white, for: .normal) btn.setBackgroundColor(.white, for: .normal)
btn.borderWidth = 0.5 btn.borderWidth = 0.5
btn.borderColor = UIColor(hexStr: "#16B3FF") btn.borderColor = UIColor(hexStr: "#16B3FF")
btn.cornerRadius = 7.5 btn.cornerRadius = 15
return btn return btn
}() }()
lazy var lineView: UIView = {
let view = UIView()
view.backgroundColor = ThemeManager.shared.color.lineColor
return view
}()
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@ -219,4 +249,9 @@ class ReviewMemberCell: UITableViewCell {
// Configure the view for the selected state // Configure the view for the selected state
} }
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
} }

View File

@ -126,25 +126,31 @@ class HomeViewController: BaseViewController {
Defaults[\.loginToken] = model.token Defaults[\.loginToken] = model.token
AppContextManager.shared.systemConfig = model.config AppContextManager.shared.systemConfig = model.config
self.getUserIMToken() self.getUserIMToken()
self.requestUserInfo() //
self.requestGroupInfo() self.requestUserInfo { [weak self] in
self?.requestGroupInfo()
}
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
/// IM Token /// IM Token
func getUserIMToken() { func getUserIMToken() {
DLToast.showLoading()
UserService.imToken().subscribe(onNext: { response in UserService.imToken().subscribe(onNext: { response in
guard let data = response.data, let token = data["token"] as? String else { return } guard let data = response.data, let token = data["token"] as? String else { return }
AppContextManager.shared.imToken = token AppContextManager.shared.imToken = token
GroupIMService.shared.login { _ in } GroupIMService.shared.login { _ in
DLToast.dismiss()
}
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
private func requestUserInfo() { private func requestUserInfo(completion: (() -> Void)? = nil) {
UserService.userInfo().subscribe { response in UserService.userInfo().subscribe { response in
guard let model = response.model else { return } guard let model = response.model else { return }
AppContextManager.shared.saveAccount(model) AppContextManager.shared.saveAccount(model)
self.rootView.avatarImgView.image = model.userIcon self.rootView.avatarImgView.image = model.userIcon
completion?()
}.disposed(by: disposeBag) }.disposed(by: disposeBag)
} }

View File

@ -71,8 +71,7 @@ class LaunchViewController: BaseViewController {
guard let model = response.model else { return } guard let model = response.model else { return }
Defaults[\.loginToken] = model.token Defaults[\.loginToken] = model.token
AppContextManager.shared.systemConfig = model.config AppContextManager.shared.systemConfig = model.config
// self.getUserIMToken()
// AppContextManager.shared.saveAccount(model)
}, onError: { [weak self] (error) in }, onError: { [weak self] (error) in
DLAlert.show(title: error.localizedDescription, DLAlert.show(title: error.localizedDescription,
defaultTitle: "重试") { [weak self] in defaultTitle: "重试") { [weak self] in
@ -82,6 +81,16 @@ class LaunchViewController: BaseViewController {
}).disposed(by: disposeBag) }).disposed(by: disposeBag)
} }
/// 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
GroupIMService.shared.login { _ in }
}).disposed(by: disposeBag)
}
// MARK: - Init // MARK: - Init
init() { init() {
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)

View File

@ -0,0 +1,172 @@
//
// CheckPermissionVC.swift
// QuickLocation
//
// Created by on 2026/6/12.
//
import UIKit
import RxSwift
import RxCocoa
class CheckPermissionVC: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setupUI()
backBtn.rx.tap.subscribe(onNext: { _ in
AppRouter.shared.popOrDismiss()
}).disposed(by: disposeBag)
tipsLab.rx.tapGesture.subscribe { _ in
AppRouter.push(Route.web, userInfo: ["url": URLManager.shared.privacyPolicyUrl])
}.disposed(by: disposeBag)
settingBtn.rx.tap.subscribe(onNext: { _ in
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return }
if UIApplication.shared.canOpenURL(settingsURL) {
UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil)
}
}).disposed(by: disposeBag)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let manager = AuthorizeManager.manager(type: .locationAlways)
settingBtn.isEnabled = manager?.authorizeStatus() != .authorized
}
private func setupUI() {
view.addSubview(navBgView)
view.addSubview(navBarView)
navBarView.addSubview(navTitleLabel)
navBarView.addSubview(backBtn)
view.addSubview(scrollView)
scrollView.addSubview(scrollContentView)
scrollContentView.addSubview(stepImgView)
view.addSubview(tipsLab)
view.addSubview(settingBtn)
navBgView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navBarView.layoutChain
.edges(excludingEdge: .bottom)
.height(kNaviHeight)
navTitleLabel.layoutChain
.top(kStatusBarHeight + 12)
.centerX()
backBtn.layoutChain
.centerY(navTitleLabel)
.left(15)
.width(24)
.height(24)
settingBtn.layoutChain
.edgesHorzontal(30)
.bottom(kSafeBottomMargin + 30)
.height(50)
tipsLab.layoutChain
.edgesHorzontal(31)
.bottomToTopOfView(settingBtn, offset: -20)
scrollView.layoutChain
.topToBottomOfView(navBarView, offset: 15)
.edgesHorzontal()
.bottomToTopOfView(tipsLab, offset: -10)
scrollContentView.layoutChain
.edges()
.widthToView(scrollView)
stepImgView.layoutChain
.top()
.edgesHorzontal(20)
.bottom()
.heightToWidth(531/335)
}
lazy var navBgView: UIImageView = {
let iv = UIImageView()
iv.image = UIImage(named: "Common/navBar_bg_2")
iv.contentMode = .scaleAspectFill
return iv
}()
lazy var navBarView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var navTitleLabel: UILabel = {
let label = UILabel()
label.text = "权限检测"
label.font = .systemFont(ofSize: 18, weight: .medium)
label.textColor = ThemeManager.shared.color.titleAuxColor
label.textAlignment = .center
return label
}()
lazy var backBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "Common/back"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 54, left: 15, bottom: 100, right: 100)
return btn
}()
lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.backgroundColor = .clear
return view
}()
lazy var scrollContentView: UIView = {
let view = UIView()
view.backgroundColor = .clear
return view
}()
lazy var stepImgView: UIImageView = {
let view = UIImageView(image: UIImage(named: "CheckPermission/permission"))
view.contentMode = .scaleAspectFill
return view
}()
lazy var tipsLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 10, weight: .regular)
label.textColor = UIColor(hexStr: "#333333")
label.textAlignment = .center
label.numberOfLines = 0
label.isUserInteractionEnabled = true
let text = "根据我们的隐私政策和用户设置,您的位置数据也将与第三方共享,用于研究、定制广告和分析目的。"
let attr = NSMutableAttributedString(string: text)
let range = (text as NSString).range(of: "隐私政策")
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: range)
label.attributedText = attr
return label
}()
lazy var settingBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("去设置", for: .normal)
btn.setTitle("已设置", for: .disabled)
btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
btn.setTitleColor(.white, for: .normal)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.cornerRadius = 25
return btn
}()
}

View File

@ -44,6 +44,8 @@ class MineViewModel {
AppRouter.push(Route.privacyPolicy) AppRouter.push(Route.privacyPolicy)
case "在线客服": case "在线客服":
this.requestWechatService() this.requestWechatService()
case "权限检测":
AppRouter.push(Route.checkPermission)
default: default:
break break
} }

View File

@ -65,7 +65,7 @@ class ScanVC: BaseViewController {
private func requestJoinGroup(code: String) { private func requestJoinGroup(code: String) {
DLToast.showLoading() DLToast.showLoading()
GroupService.operate(opType: "join", requestData: ["share_code" : code]).subscribe(onNext: { response in GroupService.operate(opType: "join", requestData: ["share_code" : code]).subscribe(onNext: { response in
DLToast.show(text: "加入成功") { DLToast.show(text: "申请成功") {
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil) NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
AppRouter.shared.popOrDismiss() AppRouter.shared.popOrDismiss()
} }

View File

@ -370,7 +370,7 @@ class FullscreenWebView: WKWebView {
if self.isHome { if self.isHome {
self.navTitle = self.webView.title ?? "" self.navTitle = self.webView.title ?? ""
} else { } else {
self.navTitle = self.webView.title ?? ""//self.webView.title == "" ? "便" : self.webView.title ?? "" self.navTitle = self.webView.title ?? ""
} }
}).disposed(by: disposeBag) }).disposed(by: disposeBag)

View File

@ -99,10 +99,26 @@ struct GroupService {
/// ///
/// - Parameters: /// - Parameters:
/// - requestDatagroup_key /// - requestDatagroup_key
static func reviewlist(requestData: [String: Any]) -> Observable<GroupInfoResponse> { static func reviewlist(requestData: [String: Any]) -> Observable<ReviewMemberListResponse> {
let api = GroupAPI.operate(opType: "reviewlist", requestData: requestData).multiTarget let api = GroupAPI.operate(opType: "reviewlist", requestData: requestData).multiTarget
return APIProvider.request(token: api) return APIProvider.request(token: api)
.map(GroupInfoResponse.self) .map(ReviewMemberListResponse.self)
.asObservable()
}
///
static func reviewMember(requestData: [String: Any]) -> Observable<ResponseModel> {
let api = GroupAPI.operate(opType: "review", requestData: requestData).multiTarget
return APIProvider.request(token: api)
.map(ResponseModel.self)
.asObservable()
}
///
static func kickMember(requestData: [String: Any]) -> Observable<ResponseModel> {
let api = GroupAPI.operate(opType: "kick", requestData: requestData).multiTarget
return APIProvider.request(token: api)
.map(ResponseModel.self)
.asObservable() .asObservable()
} }
@ -115,4 +131,12 @@ struct GroupService {
.map(GroupInfoResponse.self) .map(GroupInfoResponse.self)
.asObservable() .asObservable()
} }
///
static func dismiss(requestData: [String: Any]) -> Observable<GroupInfoResponse> {
let api = GroupAPI.operate(opType: "dismiss", requestData: requestData).multiTarget
return APIProvider.request(token: api)
.map(GroupInfoResponse.self)
.asObservable()
}
} }