登录页面开发

This commit is contained in:
zeng 2026-03-19 10:34:16 +08:00
parent 9738d46d1c
commit 40d2260102
67 changed files with 2093 additions and 127 deletions

View File

@ -162,8 +162,23 @@
F3F389062F6939EE001B0E15 /* XSRewardCoinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F389052F6939EE001B0E15 /* XSRewardCoinsViewController.swift */; };
F3F389082F6940BB001B0E15 /* XSRewardCoinsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F389072F6940BB001B0E15 /* XSRewardCoinsCell.swift */; };
F3F3890A2F694109001B0E15 /* XSSendCoinRecordModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F389092F694109001B0E15 /* XSSendCoinRecordModel.swift */; };
F3F3890C2F695055001B0E15 /* FACoinsRecordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3890B2F695055001B0E15 /* FACoinsRecordViewController.swift */; };
F3F3890E2F695068001B0E15 /* FAVipRecordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3890D2F695068001B0E15 /* FAVipRecordViewController.swift */; };
F3F3890C2F695055001B0E15 /* XSCoinsOrderRecordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3890B2F695055001B0E15 /* XSCoinsOrderRecordViewController.swift */; };
F3F3890E2F695068001B0E15 /* XSVipOrderRecordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3890D2F695068001B0E15 /* XSVipOrderRecordViewController.swift */; };
F3F389102F695C5A001B0E15 /* XSSegmentedGradientIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3890F2F695C5A001B0E15 /* XSSegmentedGradientIndicatorView.swift */; };
F3F389122F6A37E3001B0E15 /* XSCoinsOrderRecordCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F389112F6A37E3001B0E15 /* XSCoinsOrderRecordCell.swift */; };
F3F389142F6A3812001B0E15 /* XSOrderRecordModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F389132F6A3812001B0E15 /* XSOrderRecordModel.swift */; };
F3F389162F6A3AD3001B0E15 /* XSVipOrderRecordCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F389152F6A3AD3001B0E15 /* XSVipOrderRecordCell.swift */; };
F3F389182F6A4407001B0E15 /* XSStoreCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F389172F6A4407001B0E15 /* XSStoreCell.swift */; };
F3F3891A2F6A444D001B0E15 /* XSStoreCoinsBigCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F389192F6A444D001B0E15 /* XSStoreCoinsBigCell.swift */; };
F3F3891C2F6A4465001B0E15 /* XSStoreCoinsSmallCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3891B2F6A4465001B0E15 /* XSStoreCoinsSmallCell.swift */; };
F3F3891E2F6A4484001B0E15 /* XSStoreCoinsSpreadCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3891D2F6A4484001B0E15 /* XSStoreCoinsSpreadCell.swift */; };
F3F389202F6A487B001B0E15 /* XSStoreVipCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3891F2F6A487B001B0E15 /* XSStoreVipCell.swift */; };
F3F389222F6A4B8F001B0E15 /* XSCoinsPackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F389212F6A4B8F001B0E15 /* XSCoinsPackViewController.swift */; };
F3F389242F6A8B89001B0E15 /* XSHomeCoinsPackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F389232F6A8B89001B0E15 /* XSHomeCoinsPackButton.swift */; };
F3F389272F6A9597001B0E15 /* XSBaseAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F389262F6A9597001B0E15 /* XSBaseAlert.swift */; };
F3F389292F6A98C9001B0E15 /* XSAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F389282F6A98C9001B0E15 /* XSAlert.swift */; };
F3F3892B2F6AAC25001B0E15 /* XSVersionUpdateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3892A2F6AAC25001B0E15 /* XSVersionUpdateModel.swift */; };
F3F3892D2F6B86F7001B0E15 /* XSLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F3892C2F6B86F7001B0E15 /* XSLoginView.swift */; };
F3F683ED2F56C380008AF250 /* XSHomeHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F683EC2F56C380008AF250 /* XSHomeHistoryView.swift */; };
/* End PBXBuildFile section */
@ -327,8 +342,23 @@
F3F389052F6939EE001B0E15 /* XSRewardCoinsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSRewardCoinsViewController.swift; sourceTree = "<group>"; };
F3F389072F6940BB001B0E15 /* XSRewardCoinsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSRewardCoinsCell.swift; sourceTree = "<group>"; };
F3F389092F694109001B0E15 /* XSSendCoinRecordModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSendCoinRecordModel.swift; sourceTree = "<group>"; };
F3F3890B2F695055001B0E15 /* FACoinsRecordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FACoinsRecordViewController.swift; sourceTree = "<group>"; };
F3F3890D2F695068001B0E15 /* FAVipRecordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FAVipRecordViewController.swift; sourceTree = "<group>"; };
F3F3890B2F695055001B0E15 /* XSCoinsOrderRecordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSCoinsOrderRecordViewController.swift; sourceTree = "<group>"; };
F3F3890D2F695068001B0E15 /* XSVipOrderRecordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSVipOrderRecordViewController.swift; sourceTree = "<group>"; };
F3F3890F2F695C5A001B0E15 /* XSSegmentedGradientIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSSegmentedGradientIndicatorView.swift; sourceTree = "<group>"; };
F3F389112F6A37E3001B0E15 /* XSCoinsOrderRecordCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSCoinsOrderRecordCell.swift; sourceTree = "<group>"; };
F3F389132F6A3812001B0E15 /* XSOrderRecordModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSOrderRecordModel.swift; sourceTree = "<group>"; };
F3F389152F6A3AD3001B0E15 /* XSVipOrderRecordCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSVipOrderRecordCell.swift; sourceTree = "<group>"; };
F3F389172F6A4407001B0E15 /* XSStoreCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSStoreCell.swift; sourceTree = "<group>"; };
F3F389192F6A444D001B0E15 /* XSStoreCoinsBigCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSStoreCoinsBigCell.swift; sourceTree = "<group>"; };
F3F3891B2F6A4465001B0E15 /* XSStoreCoinsSmallCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSStoreCoinsSmallCell.swift; sourceTree = "<group>"; };
F3F3891D2F6A4484001B0E15 /* XSStoreCoinsSpreadCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSStoreCoinsSpreadCell.swift; sourceTree = "<group>"; };
F3F3891F2F6A487B001B0E15 /* XSStoreVipCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSStoreVipCell.swift; sourceTree = "<group>"; };
F3F389212F6A4B8F001B0E15 /* XSCoinsPackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSCoinsPackViewController.swift; sourceTree = "<group>"; };
F3F389232F6A8B89001B0E15 /* XSHomeCoinsPackButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeCoinsPackButton.swift; sourceTree = "<group>"; };
F3F389262F6A9597001B0E15 /* XSBaseAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSBaseAlert.swift; sourceTree = "<group>"; };
F3F389282F6A98C9001B0E15 /* XSAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSAlert.swift; sourceTree = "<group>"; };
F3F3892A2F6AAC25001B0E15 /* XSVersionUpdateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSVersionUpdateModel.swift; sourceTree = "<group>"; };
F3F3892C2F6B86F7001B0E15 /* XSLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSLoginView.swift; sourceTree = "<group>"; };
F3F683EC2F56C380008AF250 /* XSHomeHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XSHomeHistoryView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -482,6 +512,7 @@
F347D29C2F03A5A100786648 /* Libs */ = {
isa = PBXGroup;
children = (
F3F389252F6A955D001B0E15 /* Alert */,
F3F388EA2F680CC7001B0E15 /* IapManager */,
F3B312B52F319CA50093B180 /* Empty */,
F347D3042F0A12DB00786648 /* HUD */,
@ -560,6 +591,7 @@
F3B312B82F30B0A10093B180 /* XSSearchSuggestionCell.swift */,
F3B312B92F30B0A10093B180 /* XSSearchResultCell.swift */,
F3F683EC2F56C380008AF250 /* XSHomeHistoryView.swift */,
F3F389232F6A8B89001B0E15 /* XSHomeCoinsPackButton.swift */,
);
path = View;
sourceTree = "<group>";
@ -718,6 +750,7 @@
F3F388D42F67DDBC001B0E15 /* XSMineWalletView.swift */,
F3F388D62F67EDEC001B0E15 /* XSMineCoinsPackView.swift */,
F3F388D82F67F9E1001B0E15 /* XSMineVipView.swift */,
F3F3892C2F6B86F7001B0E15 /* XSLoginView.swift */,
);
path = View;
sourceTree = "<group>";
@ -727,6 +760,7 @@
children = (
F35547D92F3DAACB006F28CD /* XSMineItem.swift */,
F355483E2F52BD07006F28CD /* XSFeedbackCountModel.swift */,
F3F3892A2F6AAC25001B0E15 /* XSVersionUpdateModel.swift */,
);
path = Model;
sourceTree = "<group>";
@ -846,8 +880,9 @@
F3F388FD2F6931D1001B0E15 /* XSConsumptionRecordsViewController.swift */,
F3F389052F6939EE001B0E15 /* XSRewardCoinsViewController.swift */,
F3F389032F6939C7001B0E15 /* XSOrderRecordsViewController.swift */,
F3F3890B2F695055001B0E15 /* FACoinsRecordViewController.swift */,
F3F3890D2F695068001B0E15 /* FAVipRecordViewController.swift */,
F3F3890B2F695055001B0E15 /* XSCoinsOrderRecordViewController.swift */,
F3F3890D2F695068001B0E15 /* XSVipOrderRecordViewController.swift */,
F3F389212F6A4B8F001B0E15 /* XSCoinsPackViewController.swift */,
);
path = Controller;
sourceTree = "<group>";
@ -857,10 +892,18 @@
children = (
F3F388E22F680316001B0E15 /* XSStoreCoinsView.swift */,
F3F388E42F68032A001B0E15 /* XSStoreVipView.swift */,
F3F389172F6A4407001B0E15 /* XSStoreCell.swift */,
F3F389192F6A444D001B0E15 /* XSStoreCoinsBigCell.swift */,
F3F3891B2F6A4465001B0E15 /* XSStoreCoinsSmallCell.swift */,
F3F3891D2F6A4484001B0E15 /* XSStoreCoinsSpreadCell.swift */,
F3F3891F2F6A487B001B0E15 /* XSStoreVipCell.swift */,
F3F388F92F69240E001B0E15 /* XSWalletHeaderView.swift */,
F3F388FB2F692CEF001B0E15 /* XSWalletCell.swift */,
F3F388FF2F693293001B0E15 /* XSConsumptionRecordsCell.swift */,
F3F389072F6940BB001B0E15 /* XSRewardCoinsCell.swift */,
F3F3890F2F695C5A001B0E15 /* XSSegmentedGradientIndicatorView.swift */,
F3F389112F6A37E3001B0E15 /* XSCoinsOrderRecordCell.swift */,
F3F389152F6A3AD3001B0E15 /* XSVipOrderRecordCell.swift */,
);
path = View;
sourceTree = "<group>";
@ -872,6 +915,7 @@
F3F388E62F6805DE001B0E15 /* XSPayItem.swift */,
F3F389012F693785001B0E15 /* XSBuyRecordsModel.swift */,
F3F389092F694109001B0E15 /* XSSendCoinRecordModel.swift */,
F3F389132F6A3812001B0E15 /* XSOrderRecordModel.swift */,
);
path = Model;
sourceTree = "<group>";
@ -888,6 +932,15 @@
path = IapManager;
sourceTree = "<group>";
};
F3F389252F6A955D001B0E15 /* Alert */ = {
isa = PBXGroup;
children = (
F3F389262F6A9597001B0E15 /* XSBaseAlert.swift */,
F3F389282F6A98C9001B0E15 /* XSAlert.swift */,
);
path = Alert;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -1028,6 +1081,7 @@
F3F388EC2F680CEE001B0E15 /* XSIapManager.swift in Sources */,
F347D3082F0A134500786648 /* XSHud.swift in Sources */,
F347D29E2F03750000786648 /* XSTabBarItemContentView.swift in Sources */,
F3F3891E2F6A4484001B0E15 /* XSStoreCoinsSpreadCell.swift in Sources */,
F347D2FE2F0A0D0700786648 /* XSUserDefaultsKey.swift in Sources */,
F347D2EE2F0A06FB00786648 /* XSURLPath.swift in Sources */,
F347D2DC2F04EE5F00786648 /* XSHomeRankingsCell.swift in Sources */,
@ -1044,21 +1098,24 @@
F347D2E82F0A03AB00786648 /* XSNetworkModel.swift in Sources */,
F35547FE2F4EC450006F28CD /* XSMyCollectViewController.swift in Sources */,
F347D29B2F03740000786648 /* XSConfig.swift in Sources */,
F3F3892D2F6B86F7001B0E15 /* XSLoginView.swift in Sources */,
F3F388DF2F67FE6D001B0E15 /* XSStoreViewController.swift in Sources */,
F35547F42F3EFC37006F28CD /* XSMyListCollectsCell.swift in Sources */,
F347D2A72F03AAD700786648 /* XSViewController.swift in Sources */,
F3585C342F148F0800EEC469 /* XSHomeViewModel.swift in Sources */,
F347D2B02F03AE6700786648 /* XSHomeViewController.swift in Sources */,
F3F389242F6A8B89001B0E15 /* XSHomeCoinsPackButton.swift in Sources */,
F304E6102F67A9B600E9B0A6 /* XSShortDetailLockView.swift in Sources */,
F3585C3A2F14999700EEC469 /* XSProgressView.swift in Sources */,
F3F389062F6939EE001B0E15 /* XSRewardCoinsViewController.swift in Sources */,
F3585C382F1497AF00EEC469 /* XSDiscoverControlView.swift in Sources */,
F3F388D52F67DDBC001B0E15 /* XSMineWalletView.swift in Sources */,
F3F389272F6A9597001B0E15 /* XSBaseAlert.swift in Sources */,
F3F388D92F67F9E1001B0E15 /* XSMineVipView.swift in Sources */,
F347D2D42F04BF3D00786648 /* XSHomeNewTitleView.swift in Sources */,
F304E6122F67D74A00E9B0A6 /* XSVideoUnlockResult.swift in Sources */,
F3F388FE2F6931D1001B0E15 /* XSConsumptionRecordsViewController.swift in Sources */,
F3F3890C2F695055001B0E15 /* FACoinsRecordViewController.swift in Sources */,
F3F3890C2F695055001B0E15 /* XSCoinsOrderRecordViewController.swift in Sources */,
F347D28D2F03709200786648 /* AppDelegate.swift in Sources */,
F3585C492F14EE8D00EEC469 /* XSShortDetailPlayerCell.swift in Sources */,
F35548412F52BE6D006F28CD /* XSBaseWebViewController+Script.swift in Sources */,
@ -1095,19 +1152,23 @@
F347D2E02F04F7ED00786648 /* XSHomeCategoriesCell.swift in Sources */,
F3F3890A2F694109001B0E15 /* XSSendCoinRecordModel.swift in Sources */,
F347D3032F0A10B600786648 /* XSCryptorService.swift in Sources */,
F3F3891C2F6A4465001B0E15 /* XSStoreCoinsSmallCell.swift in Sources */,
F35547D42F3DA7A8006F28CD /* XSTableViewCell.swift in Sources */,
F3F388F62F68F746001B0E15 /* XSPayDataRequest.swift in Sources */,
F347D30E2F0A39DE00786648 /* XSHomeAPI.swift in Sources */,
F3F3891A2F6A444D001B0E15 /* XSStoreCoinsBigCell.swift in Sources */,
F3B312B72F319CBE0093B180 /* XSEmpty.swift in Sources */,
F347D2F62F0A0B0B00786648 /* XSLoginToken.swift in Sources */,
F347D2CA2F03DC9200786648 /* CGMutablePath+XS.swift in Sources */,
F347D2BE2F03C24B00786648 /* XSCollectionView.swift in Sources */,
F35547DA2F3DAACB006F28CD /* XSMineItem.swift in Sources */,
F3F389182F6A4407001B0E15 /* XSStoreCell.swift in Sources */,
F347D2D62F04C7D500786648 /* XSHomeNewCell.swift in Sources */,
F3F388E92F68062F001B0E15 /* XSPayDateModel.swift in Sources */,
F347D2CC2F03E04400786648 /* AppDelegate+Config.swift in Sources */,
F347D3202F0A57A300786648 /* XSDiscoverPlayerCell.swift in Sources */,
F35548022F4ECD5C006F28CD /* UIScrollView+Refresh.swift in Sources */,
F3F3892B2F6AAC25001B0E15 /* XSVersionUpdateModel.swift in Sources */,
F347D2EC2F0A060E00786648 /* XSNetworkTarget.swift in Sources */,
F3F388E12F680090001B0E15 /* XSScrollView.swift in Sources */,
F35547E92F3DDBDD006F28CD /* XSWebView.swift in Sources */,
@ -1121,24 +1182,31 @@
F3585C4B2F14FD1000EEC469 /* XSShortDetailPlayerControlView.swift in Sources */,
F3F683ED2F56C380008AF250 /* XSHomeHistoryView.swift in Sources */,
F347D2A12F03A84300786648 /* XSScreen.swift in Sources */,
F3F389202F6A487B001B0E15 /* XSStoreVipCell.swift in Sources */,
F35548062F4FD6DA006F28CD /* XSMinePlayHistoryView.swift in Sources */,
F3F3890E2F695068001B0E15 /* FAVipRecordViewController.swift in Sources */,
F3F3890E2F695068001B0E15 /* XSVipOrderRecordViewController.swift in Sources */,
F3F388E72F6805DE001B0E15 /* XSPayItem.swift in Sources */,
F3F389292F6A98C9001B0E15 /* XSAlert.swift in Sources */,
F347D2992F03730E00786648 /* XSTabBarController.swift in Sources */,
F3F389122F6A37E3001B0E15 /* XSCoinsOrderRecordCell.swift in Sources */,
F35547D22F3DA757006F28CD /* XSTableView.swift in Sources */,
F355480A2F4FE99F006F28CD /* XSMinePlayHistoryCell.swift in Sources */,
F3B312AD2F30ACF60093B180 /* XSSearchHotSectionView.swift in Sources */,
F3B312AE2F30ACF60093B180 /* XSSearchTagsView.swift in Sources */,
F3F389082F6940BB001B0E15 /* XSRewardCoinsCell.swift in Sources */,
F3B312AF2F30ACF60093B180 /* XSSearchTagCell.swift in Sources */,
F3F389142F6A3812001B0E15 /* XSOrderRecordModel.swift in Sources */,
F3B312B02F30ACF60093B180 /* XSSearchRecentView.swift in Sources */,
F35548042F4EDF27006F28CD /* UIStackView+XS.swift in Sources */,
F3B312B12F30ACF60093B180 /* XSSearchHeaderView.swift in Sources */,
F3F389162F6A3AD3001B0E15 /* XSVipOrderRecordCell.swift in Sources */,
F3B312B22F30ACF60093B180 /* XSSearchHotListItemView.swift in Sources */,
F3B312B32F30ACF60093B180 /* XSSearchGradientButton.swift in Sources */,
F3B312B42F30ACF60093B180 /* XSSearchHotListCardView.swift in Sources */,
F35547F62F3F0407006F28CD /* XSMyListHistoryView.swift in Sources */,
F3B312BF2F30B2000093B180 /* XSSearchHistoryHotView.swift in Sources */,
F3F389222F6A4B8F001B0E15 /* XSCoinsPackViewController.swift in Sources */,
F3F389102F695C5A001B0E15 /* XSSegmentedGradientIndicatorView.swift in Sources */,
F35548432F52BFB8006F28CD /* XSWebMessageModel.swift in Sources */,
F3B312BD2F30B0A10093B180 /* XSSearchSuggestionCell.swift in Sources */,
F3B312BE2F30B0A10093B180 /* XSSearchResultCell.swift in Sources */,

View File

@ -102,3 +102,22 @@ class XSTabBarController: ESTabBarController {
return nav
}
}
extension XSTabBarController {
func checkUpdates() {
Task {
guard let model = await XSSettingAPI.requestVersionUpdateData() else { return }
self.showUpdatesAlert(model)
}
}
private func showUpdatesAlert(_ model: XSVersionUpdateModel) {
}
}

View File

@ -19,6 +19,15 @@ extension String: SmartCodable {
func localizedReplace(text: String?) -> String {
return self.localized.replacingOccurrences(of: "##", with: text ?? "")
}
func localizedReplace(text1: String, text2: String, text3: String? = nil) -> String {
var string = self.localized.replacingOccurrences(of: "#1#", with: text1)
string = string.replacingOccurrences(of: "#2#", with: text2)
if let text = text3 {
string = string.replacingOccurrences(of: "#3#", with: text)
}
return string
}
}
extension String {

View File

@ -17,4 +17,14 @@ struct XSSettingAPI {
let response: XSNetwork.Response<XSFeedbackCountModel> = await XSNetwork.request(parameters: param)
return response.data
}
static func requestVersionUpdateData() async -> XSVersionUpdateModel? {
var param = XSNetwork.Parameters(path: "/customer/versionControl")
param.method = .get
param.isLoding = false
param.isToast = false
let response: XSNetwork.Response<XSVersionUpdateModel> = await XSNetwork.request(parameters: param)
return response.data
}
}

View File

@ -128,4 +128,19 @@ struct XSStoreAPI {
let response: XSNetwork.Response<XSNetwork.List<XSSendCoinRecordModel>> = await XSNetwork.request(parameters: param)
return response.data
}
static func requestRechargeRecord(page: Int, buyType: BuyType) async -> XSNetwork.List<XSOrderRecordModel>? {
let parameters: [String : Any] = [
"page_size" : 20,
"current_page" : page,
"buy_type" : buyType.rawValue
]
var param = XSNetwork.Parameters(path: "/getCustomerOrder")
param.method = .get
param.parameters = parameters
let response: XSNetwork.Response<XSNetwork.List<XSOrderRecordModel>> = await XSNetwork.request(parameters: param)
return response.data
}
}

View File

@ -19,3 +19,16 @@ class XSScrollView: UIScrollView {
}
}
class XSPagerScrollView: XSScrollView {
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let result = super.gestureRecognizerShouldBegin(gestureRecognizer)
guard let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { return result }
let translation = panGestureRecognizer.translation(in: self)
if translation.x > 0, self.contentOffset.x == 0 {
return false
}
return result
}
}

View File

@ -73,6 +73,16 @@ class XSHomeViewController: XSViewController {
return view
}()
private lazy var coinsPackButton: XSHomeCoinsPackButton = {
let view = XSHomeCoinsPackButton()
view.addAction(UIAction(handler: { [weak self] _ in
guard let self = self else { return }
let vc = XSCoinsPackViewController()
self.navigationController?.pushViewController(vc, animated: true)
}), for: .touchUpInside)
return view
}()
deinit {
NotificationCenter.default.removeObserver(self)
}
@ -121,6 +131,7 @@ extension XSHomeViewController {
view.addSubview(searchButton)
view.addSubview(segmentedView)
view.addSubview(historyView)
view.addSubview(coinsPackButton)
searchButton.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
@ -150,6 +161,11 @@ extension XSHomeViewController {
make.bottom.equalToSuperview().offset(-(XSScreen.customTabBarHeight + 5))
}
coinsPackButton.snp.makeConstraints { make in
make.right.equalToSuperview().inset(10)
make.bottom.equalTo(historyView.snp.top).offset(-20)
}
}
}

View File

@ -0,0 +1,51 @@
//
// XSHomeCoinsPackButton.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
import SnapKit
class XSHomeCoinsPackButton: UIControl {
private lazy var bgImageView = UIImageView(image: UIImage(named: "calendar_icon_01"))
private lazy var textBgImageView = UIImageView(image: UIImage(named: "button_bg_image_02"))
private lazy var textLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 10, weight: .black).withBoldItalic()
label.textColor = ._783902
label.text = "Daily Coins".localized
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(bgImageView)
addSubview(textBgImageView)
textBgImageView.addSubview(textLabel)
bgImageView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
}
textBgImageView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview()
make.top.equalToSuperview().offset(50)
}
textLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.centerX.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -135,6 +135,10 @@ extension XSMineViewController: UITableViewDelegate, UITableViewDataSource {
let vc = XSFeedbackViewController()
self.navigationController?.pushViewController(vc, animated: true)
case .wallet:
let vc = XSWalletViewController()
self.navigationController?.pushViewController(vc, animated: true)
default:
break
}
@ -145,6 +149,7 @@ extension XSMineViewController {
private func createDataArr() {
let arr = [
XSMineItem(type: .wallet, iconImage: UIImage(named: "wallet_icon_01"), title: "My Wallet".localized),
// XSMineItem(type: .setting, iconImage: UIImage(named: "setting_icon_01"), title: "Setting".localized),
XSMineItem(type: .about, iconImage: UIImage(named: "about_icon_01"), title: "About".localized),
XSMineItem(type: .feedback, iconImage: UIImage(named: "feedback_icon_01"), title: "Feedback".localized)

View File

@ -15,6 +15,7 @@ struct XSMineItem {
case feedback
case web
case safari
case wallet
///
case consumptionRecords
///

View File

@ -0,0 +1,36 @@
//
// XSVersionUpdateModel.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
import SmartCodable
struct XSVersionUpdateModel: SmartCodable {
var version_code: String?
var des: String?
var version_name: String?
var force: Bool?
func canUpdate() -> Bool {
guard let version = version_code else { return false }
let result = kXSVersion.compare(version, options: .numeric)
if result == .orderedAscending {
return true
} else {
return false
}
}
static func mappingForKey() -> [SmartKeyTransformer]? {
return [
CodingKeys.des <--- ["description"]
]
}
}

View File

@ -0,0 +1,175 @@
//
// XSLoginView.swift
// XSeri
//
// Created by 鸿 on 2026/3/19.
//
import UIKit
import SnapKit
import YYText
class XSLoginView: XSPanModalContentView {
private lazy var bgView: UIImageView = {
let view = UIImageView(image: UIImage(named: "bg_image_01"))
return view
}()
private lazy var closeButton: UIButton = {
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
self.dismiss(animated: true) { }
}))
button.setImage(UIImage(named: "close_icon_02"), for: .normal)
return button
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 18, weight: .semibold)
label.textColor = .white
label.text = "Welcome to XSeri"
return label
}()
private lazy var stackView: UIStackView = {
let view = UIStackView(arrangedSubviews: [facebookButton, appleButton])
view.axis = .vertical
view.spacing = 18
return view
}()
private lazy var facebookButton: UIButton = {
var configuration = UIButton.Configuration.plain()
configuration.background.cornerRadius = 8
configuration.background.backgroundColor = ._0866_FF
configuration.imagePadding = 12
configuration.image = UIImage(named: "facebook_icon_01")
configuration.attributedTitle = AttributedString("Login with Facebook".localized, attributes: AttributeContainer([
.font : UIFont.font(ofSize: 14, weight: .semibold),
.foregroundColor : UIColor.white
]))
let button = UIButton(configuration: configuration, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
}))
button.snp.makeConstraints { make in
make.height.equalTo(48)
}
return button
}()
private lazy var appleButton: UIButton = {
var configuration = UIButton.Configuration.plain()
configuration.background.cornerRadius = 8
configuration.background.backgroundColor = .white
configuration.imagePadding = 12
configuration.image = UIImage(named: "apple_icon_01")
configuration.attributedTitle = AttributedString("Login with Apple".localized, attributes: AttributeContainer([
.font : UIFont.font(ofSize: 14, weight: .semibold),
.foregroundColor : UIColor._333333
]))
let button = UIButton(configuration: configuration, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
}))
button.snp.makeConstraints { make in
make.height.equalTo(48)
}
return button
}()
private lazy var agreementLabel: YYLabel = {
let userAgreementText = "User Agreement".localized
let privacyPolicyText = "Privacy Policy".localized
let text = "By continuing, you agree to the\n#1# and #2#".localizedReplace(text1: userAgreementText, text2: privacyPolicyText)
let userAgreementRange = (text as NSString).range(of: userAgreementText)
let privacyPolicyRange = (text as NSString).range(of: privacyPolicyText)
let string = NSMutableAttributedString(string: text, attributes: [
.font : UIFont.font(ofSize: 10, weight: .semibold),
.foregroundColor : UIColor.FFDAA_4
])
string.yy_setTextHighlight(userAgreementRange, color: .white, backgroundColor: nil) { [weak self] _, _, _, _ in
guard let self = self else { return }
let vc = XSBaseWebViewController()
vc.webUrl = kXSUserAgreementWebUrl
XSTool.topViewController?.navigationController?.pushViewController(vc, animated: true)
self.dismiss(animated: true) {
}
}
string.yy_setTextHighlight(privacyPolicyRange, color: .white, backgroundColor: nil) { [weak self] _, _, _, _ in
guard let self = self else { return }
let vc = XSBaseWebViewController()
vc.webUrl = kXSPrivacyPolicyWebUrl
XSTool.topViewController?.navigationController?.pushViewController(vc, animated: true)
self.dismiss(animated: true) {
}
}
let label = YYLabel()
label.attributedText = string
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.contentHeight = XSScreen.safeBottom + 320
xs_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension XSLoginView {
private func xs_setupUI() {
addSubview(bgView)
addSubview(closeButton)
addSubview(titleLabel)
addSubview(stackView)
addSubview(agreementLabel)
bgView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
}
closeButton.snp.makeConstraints { make in
make.right.equalToSuperview().inset(23)
make.top.equalToSuperview().offset(20)
}
titleLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(closeButton.snp.bottom).offset(12)
}
stackView.snp.makeConstraints { make in
make.width.equalTo(300)
make.centerX.equalToSuperview()
make.top.equalTo(closeButton.snp.bottom).offset(75)
}
agreementLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().inset(XSScreen.safeBottom + 20)
}
}
}

View File

@ -44,6 +44,14 @@ class XSMineCoinsPackView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
let tap = UITapGestureRecognizer { [weak self] _ in
guard let self = self else { return }
let vc = XSCoinsPackViewController()
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}
bgView.addGestureRecognizer(tap)
xs_setupUI()
}

View File

@ -15,6 +15,7 @@ class XSMineUserInfoView: UIView {
avatarImageView.xs_setImage(userInfo?.avator, placeholder: UIImage(named: "avatar_placeholder_icon_01"))
nickNameLabel.text = userInfo?.getNickName()
idLabel.text = "ID: \(userInfo?.customer_id ?? "")"
loginButton.isHidden = !(userInfo?.is_tourist ?? true)
}
}
@ -38,6 +39,22 @@ class XSMineUserInfoView: UIView {
return label
}()
private lazy var loginButton: UIButton = {
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
let view = XSLoginView()
view.present(in: nil)
}))
button.layer.cornerRadius = 16
button.layer.masksToBounds = true
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.white.withAlphaComponent(0.14).cgColor
button.setTitle("Login".localized, for: .normal)
button.setTitleColor(.white.withAlphaComponent(0.7), for: .normal)
button.titleLabel?.font = .font(ofSize: 14, weight: .semibold)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
@ -56,6 +73,7 @@ extension XSMineUserInfoView {
addSubview(avatarImageView)
addSubview(nickNameLabel)
addSubview(idLabel)
addSubview(loginButton)
avatarImageView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
@ -74,6 +92,13 @@ extension XSMineUserInfoView {
make.bottom.equalTo(avatarImageView).offset(-6)
}
loginButton.snp.makeConstraints { make in
make.centerY.equalTo(avatarImageView)
make.right.equalToSuperview().inset(16)
make.width.equalTo(86)
make.height.equalTo(32)
}
}
}

View File

@ -1,29 +0,0 @@
//
// FACoinsRecordViewController.swift
// XSeri
//
// Created by 鸿 on 2026/3/17.
//
import UIKit
class FACoinsRecordViewController: XSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}

View File

@ -1,29 +0,0 @@
//
// FAVipRecordViewController.swift
// XSeri
//
// Created by 鸿 on 2026/3/17.
//
import UIKit
class FAVipRecordViewController: XSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}

View File

@ -0,0 +1,108 @@
//
// XSCoinsOrderRecordViewController.swift
// XSeri
//
// Created by 鸿 on 2026/3/17.
//
import UIKit
import SnapKit
class XSCoinsOrderRecordViewController: XSViewController {
private lazy var dataArr: [XSOrderRecordModel] = []
private lazy var page = 1
private lazy var tableView: XSTableView = {
let tableView = XSTableView(frame: .zero, style: .plain)
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = 70
tableView.separatorColor = .white.withAlphaComponent(0.1)
tableView.separatorInset = .init(top: 0, left: 16, bottom: 0, right: 16)
tableView.contentInset = .init(top: 10, left: 0, bottom: XSScreen.safeBottom + 10, right: 0)
tableView.register(XSCoinsOrderRecordCell.self, forCellReuseIdentifier: "cell")
tableView.ly_emptyView = XSEmpty.xs_emptyView()
tableView.xs_addRefreshHeader(insetTop: tableView.contentInset.top) { [weak self] in
self?.handleHeaderRefresh(nil)
}
tableView.xs_addRefreshFooter(insetBottom: tableView.contentInset.bottom) { [weak self] in
self?.handleFooterRefresh(nil)
}
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .clear
xs_setupUI()
Task {
await requestDataArr(page: 1)
}
}
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
Task {
await requestDataArr(page: 1)
self.tableView.xs_endHeaderRefreshing()
}
}
override func handleFooterRefresh(_ completer: (() -> Void)?) {
Task {
await requestDataArr(page: self.page + 1)
self.tableView.xs_endFooterRefreshing()
}
}
}
extension XSCoinsOrderRecordViewController {
private func xs_setupUI() {
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(1)
}
}
}
//MARK: UITableViewDelegate UITableViewDataSource
extension XSCoinsOrderRecordViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! XSCoinsOrderRecordCell
cell.model = dataArr[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataArr.count
}
}
extension XSCoinsOrderRecordViewController {
private func requestDataArr(page: Int) async {
guard let listModel = await XSStoreAPI.requestRechargeRecord(page: page, buyType: .coins) else { return }
guard let list = listModel.list else { return }
if page == 1 {
self.dataArr.removeAll()
}
self.dataArr += list
self.page = page
self.tableView.reloadData()
}
}

View File

@ -0,0 +1,25 @@
//
// XSCoinsPackViewController.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
class XSCoinsPackViewController: XSViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
xs_setNavigationStyle()
}
}

View File

@ -11,9 +11,10 @@ import JXSegmentedView
class XSOrderRecordsViewController: XSViewController {
private var segmentedItemWidth: CGFloat = 0
private lazy var viewControllers: [XSViewController] = {
return [FACoinsRecordViewController(), FAVipRecordViewController()]
return [XSCoinsOrderRecordViewController(), XSVipOrderRecordViewController()]
}()
private lazy var bgImageView: UIImageView = {
@ -21,27 +22,53 @@ class XSOrderRecordsViewController: XSViewController {
return imageView
}()
private lazy var segmentedContainerView: UIView = {
let view = UIView()
view.backgroundColor = .white.withAlphaComponent(0.16)
view.layer.cornerRadius = 16
view.layer.masksToBounds = true
return view
}()
/// Segmented
private lazy var segmentedDataSource: JXSegmentedTitleDataSource = {
let dataSource = JXSegmentedTitleDataSource()
dataSource.titles = ["Coin Record".localized, "VIP Record".localized]
dataSource.titleNormalColor = UIColor.white.withAlphaComponent(0.55)
dataSource.titleSelectedColor = .white
dataSource.titleNormalFont = .font(ofSize: 16, weight: .medium)
dataSource.titleSelectedColor = ._00181_A
dataSource.titleNormalFont = .font(ofSize: 16, weight: .semibold)
dataSource.titleSelectedFont = .font(ofSize: 16, weight: .semibold)
dataSource.isTitleColorGradientEnabled = true
dataSource.itemSpacing = 24
dataSource.isItemSpacingAverageEnabled = false
dataSource.itemSpacing = 0
return dataSource
}()
private lazy var segmentedIndicator: XSSegmentedGradientIndicatorView = {
let indicator = XSSegmentedGradientIndicatorView()
indicator.gradientColors = [
UIColor.F_5_BD_7_E.cgColor,
UIColor.FFEABC.cgColor,
UIColor.FFCF_99.cgColor
]
indicator.gradientLocations = [0, 0.5, 1]
indicator.startPoint = CGPoint(x: 0, y: 0.5)
indicator.endPoint = CGPoint(x: 1, y: 0.5)
indicator.indicatorHeight = 36
indicator.indicatorCornerRadius = 18
indicator.indicatorWidthIncrement = 0
return indicator
}()
/// Segmented (Tab )
private lazy var segmentedView: JXSegmentedView = {
let view = JXSegmentedView()
view.dataSource = segmentedDataSource
view.delegate = self
view.listContainer = listContainerView
view.contentEdgeInsetLeft = 16
view.contentEdgeInsetRight = 16
view.contentEdgeInsetLeft = 6
view.contentEdgeInsetRight = 6
view.indicators = [segmentedIndicator]
view.backgroundColor = .clear
return view
}()
@ -53,7 +80,8 @@ class XSOrderRecordsViewController: XSViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Order Records".localized
self.view.backgroundColor = .black
xs_setupUI()
}
@ -64,6 +92,18 @@ class XSOrderRecordsViewController: XSViewController {
xs_setNavigationStyle()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let horizontalInset = segmentedView.contentEdgeInsetLeft + segmentedView.contentEdgeInsetRight
let availableWidth = segmentedContainerView.bounds.width - horizontalInset
let itemWidth = floor(availableWidth / CGFloat(max(segmentedDataSource.titles.count, 1)))
guard itemWidth > 0, segmentedItemWidth != itemWidth else { return }
segmentedItemWidth = itemWidth
segmentedDataSource.itemWidth = itemWidth
segmentedView.reloadDataWithoutListContainer()
}
}
@ -71,15 +111,27 @@ extension XSOrderRecordsViewController {
private func xs_setupUI() {
view.addSubview(bgImageView)
view.addSubview(segmentedContainerView)
segmentedContainerView.addSubview(segmentedView)
view.addSubview(listContainerView)
bgImageView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
}
segmentedContainerView.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(16)
make.top.equalTo(self.view.safeAreaLayoutGuide).offset(10)
make.height.equalTo(48)
}
segmentedView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
listContainerView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalTo(self.view.safeAreaLayoutGuide)
make.top.equalTo(segmentedContainerView.snp.bottom).offset(8)
}
}
@ -101,4 +153,9 @@ extension XSOrderRecordsViewController: JXSegmentedListContainerViewDataSource {
func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> JXSegmentedListContainerViewListDelegate {
return viewControllers[index]
}
func scrollViewClass(in listContainerView: JXSegmentedListContainerView) -> AnyClass {
return XSPagerScrollView.self
}
}

View File

@ -9,6 +9,9 @@ import UIKit
class XSStoreViewController: XSViewController {
private var payRequest: XSPayDataRequest?
private var payDataModel: XSPayDateModel? = XSIapManager.manager.payDateModel
private lazy var scrollView: XSScrollView = {
let scrollView = XSScrollView()
@ -22,12 +25,32 @@ class XSStoreViewController: XSViewController {
return view
}()
private lazy var coinsView: XSStoreCoinsView = {
let view = XSStoreCoinsView()
view.buyFinishHandle = { [weak self] in
self?.buyFinish()
}
return view
}()
private lazy var vipView: XSStoreVipView = {
let view = XSStoreVipView()
view.buyFinishHandle = { [weak self] in
self?.buyFinish()
}
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Store".localized
xs_setupUI()
reloadData()
requestPayData()
}
@ -37,6 +60,50 @@ class XSStoreViewController: XSViewController {
xs_setNavigationStyle()
}
func reloadData() {
self.stackView.xs_removeAllArrangedSubview()
guard let model = self.payDataModel else { return }
if let sort = model.sort, sort.count > 0 {
sort.forEach {
if $0 == .vip {
self.addVipView()
} else if $0 == .coin {
self.addCoinsView()
}
}
} else {
self.addVipView()
self.addCoinsView()
}
// self.stackView.addArrangedSubview(tipView)
}
private func addCoinsView() {
guard let model = self.payDataModel else { return }
var newList: [XSPayItem] = []
if let list = model.list_coins, list.count > 0 {
newList = list
}
if newList.count > 0 {
coinsView.setDataArr(newList)
self.stackView.addArrangedSubview(coinsView)
}
}
private func addVipView() {
guard let list = payDataModel?.list_sub_vip else { return }
guard list.count > 0 else { return }
self.vipView.dataArr = list
self.stackView.addArrangedSubview(self.vipView)
}
private func buyFinish() {
self.requestPayData()
}
}
extension XSStoreViewController {
@ -58,3 +125,21 @@ extension XSStoreViewController {
}
}
extension XSStoreViewController {
private func requestPayData() {
payRequest = XSPayDataRequest()
payRequest?.requestProducts { [weak self] model in
guard let self = self else { return }
guard let model = model else { return }
self.payDataModel = model
self.reloadData()
}
}
}

View File

@ -0,0 +1,107 @@
//
// XSVipOrderRecordViewController.swift
// XSeri
//
// Created by 鸿 on 2026/3/17.
//
import UIKit
class XSVipOrderRecordViewController: XSViewController {
private lazy var dataArr: [XSOrderRecordModel] = []
private lazy var page = 1
private lazy var tableView: XSTableView = {
let tableView = XSTableView(frame: .zero, style: .plain)
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = 70
tableView.separatorColor = .white.withAlphaComponent(0.1)
tableView.separatorInset = .init(top: 0, left: 16, bottom: 0, right: 16)
tableView.contentInset = .init(top: 10, left: 0, bottom: XSScreen.safeBottom + 10, right: 0)
tableView.register(XSVipOrderRecordCell.self, forCellReuseIdentifier: "cell")
tableView.ly_emptyView = XSEmpty.xs_emptyView()
tableView.xs_addRefreshHeader(insetTop: tableView.contentInset.top) { [weak self] in
self?.handleHeaderRefresh(nil)
}
tableView.xs_addRefreshFooter(insetBottom: tableView.contentInset.bottom) { [weak self] in
self?.handleFooterRefresh(nil)
}
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .clear
xs_setupUI()
Task {
await requestDataArr(page: 1)
}
}
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
Task {
await requestDataArr(page: 1)
self.tableView.xs_endHeaderRefreshing()
}
}
override func handleFooterRefresh(_ completer: (() -> Void)?) {
Task {
await requestDataArr(page: self.page + 1)
self.tableView.xs_endFooterRefreshing()
}
}
}
extension XSVipOrderRecordViewController {
private func xs_setupUI() {
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(1)
}
}
}
//MARK: UITableViewDelegate UITableViewDataSource
extension XSVipOrderRecordViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! XSVipOrderRecordCell
cell.model = dataArr[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataArr.count
}
}
extension XSVipOrderRecordViewController {
private func requestDataArr(page: Int) async {
guard let listModel = await XSStoreAPI.requestRechargeRecord(page: page, buyType: .vip) else { return }
guard let list = listModel.list else { return }
if page == 1 {
self.dataArr.removeAll()
}
self.dataArr += list
self.page = page
self.tableView.reloadData()
}
}

View File

@ -46,6 +46,7 @@ class XSWalletViewController: XSViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Details".localized
self.view.backgroundColor = .black
NotificationCenter.default.addObserver(self, selector: #selector(loginStateDidChangeNotification), name: XSLoginManager.loginStateDidChangeNotification, object: nil)
xs_setupUI()
@ -124,6 +125,9 @@ extension XSWalletViewController: UITableViewDelegate, UITableViewDataSource {
case .rewardCoins:
vc = XSRewardCoinsViewController()
case .feedback:
vc = XSFeedbackViewController()
default:
break
}

View File

@ -0,0 +1,16 @@
//
// XSOrderRecordModel.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
import SmartCodable
struct XSOrderRecordModel: SmartCodable {
var type: String?
var value: String?
var created_at: String?
}

View File

@ -0,0 +1,80 @@
//
// XSCoinsOrderRecordCell.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
import SnapKit
class XSCoinsOrderRecordCell: XSTableViewCell {
var model: XSOrderRecordModel? {
didSet {
titleLabel.text = model?.type
dateLabel.text = model?.created_at
coinsLabel.text = "+\(model?.value ?? "0")"
}
}
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .white
return label
}()
private lazy var dateLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .regular)
label.textColor = .FFDAA_4
return label
}()
private lazy var coinsIconImageView = UIImageView(image: UIImage(named: "coins_icon_03"))
private lazy var coinsLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .white
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(titleLabel)
contentView.addSubview(dateLabel)
contentView.addSubview(coinsIconImageView)
contentView.addSubview(coinsLabel)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(32)
make.top.equalToSuperview().offset(15)
}
dateLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.bottom.equalToSuperview().offset(-15)
}
coinsLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-25)
}
coinsIconImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalTo(coinsLabel.snp.left).offset(-4)
}
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,57 @@
//
// XSSegmentedGradientIndicatorView.swift
// XSeri
//
// Created by 鸿 on 2026/3/17.
//
import UIKit
import JXSegmentedView
class XSSegmentedGradientIndicatorView: JXSegmentedIndicatorBackgroundView {
var gradientColors: [CGColor] = [] {
didSet {
gradientLayer.colors = gradientColors
}
}
var gradientLocations: [NSNumber]? {
didSet {
gradientLayer.locations = gradientLocations
}
}
var startPoint: CGPoint = CGPoint(x: 0, y: 0.5) {
didSet {
gradientLayer.startPoint = startPoint
}
}
var endPoint: CGPoint = CGPoint(x: 1, y: 0.5) {
didSet {
gradientLayer.endPoint = endPoint
}
}
private let gradientLayer = CAGradientLayer()
override func commonInit() {
super.commonInit()
indicatorColor = .clear
backgroundColor = .clear
layer.insertSublayer(gradientLayer, at: 0)
}
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = .clear
gradientLayer.frame = bounds
gradientLayer.cornerRadius = layer.cornerRadius
gradientLayer.colors = gradientColors
gradientLayer.locations = gradientLocations
gradientLayer.startPoint = startPoint
gradientLayer.endPoint = endPoint
}
}

View File

@ -0,0 +1,15 @@
//
// XSStoreCell.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
class XSStoreCell: UICollectionViewCell {
var item: XSPayItem?
var xs_isSelected: Bool?
}

View File

@ -0,0 +1,29 @@
//
// XSStoreCoinsBigCell.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
class XSStoreCoinsBigCell: XSStoreCell {
private lazy var bgView: XSView = {
let view = XSView()
view.xs_colors = []
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,12 @@
//
// XSStoreCoinsSmallCell.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
class XSStoreCoinsSmallCell: XSStoreCell {
}

View File

@ -0,0 +1,12 @@
//
// XSStoreCoinsSpreadCell.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
class XSStoreCoinsSpreadCell: XSStoreCell {
}

View File

@ -10,6 +10,14 @@ import SnapKit
class XSStoreCoinsView: UIView {
var buyFinishHandle: (() -> Void)?
var shortPlayId: String?
var videoId: String?
private lazy var dataArr: [[XSPayItem]] = []
private var selectedIndexPath: IndexPath?
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .bold)
@ -18,39 +26,43 @@ class XSStoreCoinsView: UIView {
return label
}()
// private lazy var collectionViewLayout: UICollectionViewCompositionalLayout = {
// let config = UICollectionViewCompositionalLayoutConfiguration()
// config.interSectionSpacing = 10
//
// let layout = UICollectionViewCompositionalLayout { [weak self] section, _ in
// guard let self = self else { return nil}
// guard let model = dataArr[section].first else { return nil }
//
// if model.buy_type == .subCoins {
// return self.coinsBigLayoutSection()
// } else if model.size == .big {
// return self.bigLayoutSection()
// } else {
// return self.smallLayoutSection()
// }
// }
// layout.configuration = config
//
// return layout
// }()
//
// private lazy var collectionView: FACollectionView = {
// let collectionView = FACollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
// collectionView.delegate = self
// collectionView.dataSource = self
// collectionView.clipsToBounds = false
// collectionView.isScrollEnabled = false
// collectionView.register(FAStoreCoinsBigCell.self, forCellWithReuseIdentifier: "FAStoreCoinsBigCell")
// collectionView.register(FAStoreCoinsSmallCell.self, forCellWithReuseIdentifier: "FAStoreCoinsSmallCell")
// collectionView.register(FAStoreCoinsPackCell.self, forCellWithReuseIdentifier: "FAStoreCoinsPackCell")
// collectionView.addObserver(self, forKeyPath: "contentSize", context: nil)
// return collectionView
// }()
private lazy var collectionViewLayout: UICollectionViewCompositionalLayout = {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 12
let layout = UICollectionViewCompositionalLayout { [weak self] section, environment in
guard let self = self else { return nil}
guard let model = dataArr[section].first else { return nil }
if model.size == .spread {
return self.spreadLayoutSection(environment)
} else if model.size == .big {
return self.bigLayoutSection(environment)
} else {
return self.smallLayoutSection(environment)
}
}
layout.configuration = config
return layout
}()
private lazy var collectionView: XSCollectionView = {
let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.clipsToBounds = false
collectionView.isScrollEnabled = false
collectionView.register(XSStoreCoinsBigCell.self, forCellWithReuseIdentifier: XSPayItem.SizeType.big.rawValue)
collectionView.register(XSStoreCoinsSmallCell.self, forCellWithReuseIdentifier: XSPayItem.SizeType.small.rawValue)
collectionView.register(XSStoreCoinsSpreadCell.self, forCellWithReuseIdentifier: XSPayItem.SizeType.spread.rawValue)
collectionView.addObserver(self, forKeyPath: "contentSize", context: nil)
return collectionView
}()
deinit {
collectionView.removeObserver(self, forKeyPath: "contentSize")
}
override init(frame: CGRect) {
super.init(frame: frame)
@ -61,17 +73,153 @@ class XSStoreCoinsView: UIView {
fatalError("init(coder:) has not been implemented")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
let height = self.collectionView.contentSize.height + 1
self.collectionView.snp.updateConstraints { make in
make.height.equalTo(height)
}
}
}
func setDataArr(_ arr: [XSPayItem]) {
self.dataArr.removeAll()
var bigArr: [XSPayItem] = []
var smallArr: [XSPayItem] = []
var spreadArr: [XSPayItem] = []
arr.forEach {
if $0.size == .spread {
spreadArr.append($0)
} else if $0.size == .big {
bigArr.append($0)
} else {
smallArr.append($0)
}
}
if bigArr.count > 0 {
self.dataArr.append(bigArr)
}
if spreadArr.count > 0 {
self.dataArr.append(spreadArr)
}
if smallArr.count > 0 {
self.dataArr.append(smallArr)
}
self.collectionView.reloadData()
}
}
extension XSStoreCoinsView {
private func xs_setupUI() {
addSubview(titleLabel)
addSubview(collectionView)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.top.equalToSuperview()
}
collectionView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(36)
make.left.right.equalToSuperview().inset(16)
make.height.equalTo(1)
}
}
}
extension XSStoreCoinsView {
private func bigLayoutSection(_ environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection {
let contentWidth = environment.container.effectiveContentSize.width
let itemWidth = floor((contentWidth - 15) / 2)
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .absolute(itemWidth), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(84)), subitems: [item])
group.interItemSpacing = .fixed(15)
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = 12
layoutSection.contentInsets = .zero
return layoutSection
}
private func smallLayoutSection(_ environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection {
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1 / 3), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(121)), subitems: [item])
group.interItemSpacing = .fixed(8)
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = 10
layoutSection.contentInsets = .zero
return layoutSection
}
private func spreadLayoutSection(_ environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection {
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(84)), subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = 10
layoutSection.contentInsets = .zero
return layoutSection
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension XSStoreCoinsView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let item = dataArr[indexPath.section][indexPath.row]
var identifier = item.size?.rawValue ?? ""
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! XSStoreCell
cell.item = item
cell.xs_isSelected = selectedIndexPath == indexPath
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArr[section].count
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return dataArr.count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = dataArr[indexPath.section][indexPath.row]
self.selectedIndexPath = indexPath
collectionView.reloadData()
// if model.buy_type == .subCoins {
// let view = FACoinPackConfirmView()
// view.shortPlayId = self.shortPlayId
// view.videoId = self.videoId
// view.model = model
// view.buyFinishHandle = { [weak self] in
// guard let self = self else { return }
// FALogin.manager.requestUserInfo(completer: nil)
// self.buyFinishHandle?()
// }
// view.present(in: nil)
// } else {
// FAIapManager.manager.start(model: model, shortPlayId: self.shortPlayId, videoId: self.videoId) { [weak self] finish in
// guard let self = self else { return }
// if finish {
// FALogin.manager.requestUserInfo(completer: nil)
// self.buyFinishHandle?()
// }
// }
// }
}
}

View File

@ -0,0 +1,12 @@
//
// XSStoreVipCell.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
class XSStoreVipCell: XSStoreCell {
}

View File

@ -6,9 +6,140 @@
//
import UIKit
import SnapKit
class XSStoreVipView: UIView {
var buyFinishHandle: (() -> Void)?
var shortPlayId: String?
var videoId: String?
var dataArr: [XSPayItem] = [] {
didSet {
collectionView.reloadData()
}
}
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .bold)
label.textColor = .white
label.text = "VIP Membership".localized
return label
}()
private lazy var subtitleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .regular)
label.textColor = .white.withAlphaComponent(0.5)
label.text = "Auto renew,cancel anytime".localized
return label
}()
private lazy var collectionViewLayout: UICollectionViewCompositionalLayout = {
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(110)), subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = 14
layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
let config = UICollectionViewCompositionalLayoutConfiguration()
let layout = UICollectionViewCompositionalLayout(section: layoutSection)
layout.configuration = config
return layout
}()
private lazy var collectionView: XSCollectionView = {
let collectionView = XSCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.isScrollEnabled = false
collectionView.addObserver(self, forKeyPath: "contentSize", context: nil)
collectionView.register(XSStoreVipCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
deinit {
collectionView.removeObserver(self, forKeyPath: "contentSize")
}
override init(frame: CGRect) {
super.init(frame: frame)
xs_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
let height = self.collectionView.contentSize.height + 1
self.collectionView.snp.updateConstraints { make in
make.height.equalTo(height)
}
}
}
}
extension XSStoreVipView {
private func xs_setupUI() {
addSubview(titleLabel)
addSubview(subtitleLabel)
addSubview(collectionView)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.top.equalToSuperview()
}
subtitleLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.top.equalTo(titleLabel.snp.bottom).offset(0)
}
collectionView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(54)
make.height.equalTo(1)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension XSStoreVipView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! XSStoreVipCell
cell.item = self.dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArr.count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = self.dataArr[indexPath.row]
// FAIapManager.manager.start(model: model, shortPlayId: self.shortPlayId, videoId: self.videoId) { [weak self] finish in
// guard let self = self else { return }
// if finish {
// FALogin.manager.requestUserInfo(completer: nil)
// self.buyFinishHandle?()
// }
// }
}
}

View File

@ -0,0 +1,71 @@
//
// XSVipOrderRecordCell.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
class XSVipOrderRecordCell: XSTableViewCell {
var model: XSOrderRecordModel? {
didSet {
titleLabel.text = model?.type
dateLabel.text = model?.created_at
countLabel.text = "+\(model?.value ?? "0")"
}
}
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .white
return label
}()
private lazy var dateLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .regular)
label.textColor = .FFDAA_4
return label
}()
private lazy var countLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .white
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(titleLabel)
contentView.addSubview(dateLabel)
contentView.addSubview(countLabel)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(32)
make.top.equalToSuperview().offset(15)
}
dateLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.bottom.equalToSuperview().offset(-15)
}
countLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-25)
}
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,159 @@
//
// XSAlert.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
import SnapKit
class XSAlert: XSBaseAlert {
private var contentBorderImage = UIImage(named: "alert_bg_border_color_image")
private var buttonText: String? {
didSet {
self.highlightButton.updateConfiguration()
}
}
private lazy var titleBgView: UIView = {
let view = XSView()
view.xs_colors = [UIColor.black.withAlphaComponent(0).cgColor, UIColor.FFDAA_4.withAlphaComponent(0.34).cgColor, UIColor.black.withAlphaComponent(0).cgColor]
view.xs_startPoint = .init(x: 0, y: 0.5)
view.xs_endPoint = .init(x: 1, y: 0.5)
return view
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 18, weight: .semibold)
label.textColor = .FFDAA_4
return label
}()
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
return imageView
}()
private lazy var textLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .white
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
private lazy var highlightButton: UIButton = {
var configuration = UIButton.Configuration.plain()
let button = XSButton(configuration: configuration, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
self.handleHighlightButton()
}))
button.xs_colors = [UIColor._0_C_0701.withAlphaComponent(0).cgColor, UIColor._16110_E.cgColor, UIColor._574537.cgColor]
button.xs_startPoint = .init(x: 0.5, y: 0)
button.xs_endPoint = .init(x: 0.5, y: 1)
button.layer.cornerRadius = 24
button.layer.masksToBounds = true
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.FFDAA_4.cgColor
button.configurationUpdateHandler = { [weak self] button in
guard let self = self else { return }
var configuration = button.configuration
configuration?.attributedTitle = AttributedString(self.buttonText ?? "", attributes: AttributeContainer([
.font : UIFont.font(ofSize: 14, weight: .semibold),
.foregroundColor : UIColor.FFDAA_4
]))
button.configuration = configuration
}
return button
}()
deinit {
self.contentView.removeObserver(self, forKeyPath: "bounds")
}
override init(frame: CGRect) {
super.init(frame: frame)
self.contentView.backgroundColor = .black
self.contentView.layer.cornerRadius = 36
self.contentView.layer.masksToBounds = true
self.contentView.layer.borderWidth = 1
self.contentView.addObserver(self, forKeyPath: "bounds", context: nil)
xs_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "bounds" {
self.contentBorderImage = self.contentBorderImage?.xs_resized(to: self.contentView.bounds.size)
self.contentView.layer.borderColor = UIColor(patternImage: self.contentBorderImage!).cgColor
}
}
func show(title: String, text: String, image: UIImage?, buttonText: String) -> Self {
self.titleLabel.text = title
self.textLabel.text = text
self.imageView.image = image
self.buttonText = buttonText
return self
}
}
extension XSAlert {
private func xs_setupUI() {
contentView.addSubview(titleBgView)
titleBgView.addSubview(titleLabel)
contentView.addSubview(imageView)
contentView.addSubview(textLabel)
contentView.addSubview(highlightButton)
titleBgView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.left.right.equalToSuperview().inset(23)
make.height.equalTo(48)
make.width.equalTo(256)
}
titleLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
}
imageView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.centerY.equalTo(self.contentView.snp.top).inset(131)
}
textLabel.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.right.lessThanOrEqualToSuperview().offset(-45)
make.top.equalTo(titleBgView.snp.bottom).offset(152)
make.height.greaterThanOrEqualTo(36)
}
highlightButton.snp.makeConstraints { make in
make.right.left.equalToSuperview().inset(40)
make.top.equalTo(textLabel.snp.bottom).offset(18)
make.bottom.equalToSuperview().offset(-17)
make.height.equalTo(48)
}
}
}

View File

@ -0,0 +1,143 @@
//
// XSBaseAlert.swift
// XSeri
//
// Created by 鸿 on 2026/3/18.
//
import UIKit
import SnapKit
class XSBaseAlert: UIView {
var clickHighlightButton: (() -> Void)?
private(set) var containerView: UIView = {
let view = UIView()
return view
}()
private(set) var contentView: UIView = {
let view = UIView()
return view
}()
private(set) lazy var closeButton: UIButton = {
let button = UIButton(type: .custom)
button.setImage(UIImage(named: "close_icon_02"), for: .normal)
button.addTarget(self, action: #selector(dismiss), for: .touchUpInside)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .black.withAlphaComponent(0.5)
addSubview(containerView)
containerView.addSubview(contentView)
containerView.addSubview(closeButton)
containerView.snp.makeConstraints { make in
make.center.equalToSuperview()
}
contentView.snp.makeConstraints { make in
make.left.right.top.equalToSuperview()
make.bottom.equalToSuperview().offset(-76)
}
closeButton.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.bottom.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@discardableResult
@objc func show(in view: UIView? = nil) -> Self {
guard self.superview == nil else { return self }
var inView: UIView
if let view = view {
inView = view
} else {
inView = XSBaseAlert.Window.manager.createWindow()
}
inView.addSubview(self)
self.frame = inView.bounds
showAnimation()
return self
}
@objc func dismiss() {
dismissAnimation()
}
@objc func handleHighlightButton() {
self.dismissAnimation()
self.clickHighlightButton?()
}
}
extension XSBaseAlert {
private func showAnimation() {
containerView.transform = CGAffineTransform(translationX: 0, y: 200)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0) {
self.containerView.transform = CGAffineTransform.identity
}
}
private func dismissAnimation() {
UIView.animate(withDuration: 0.3) {
self.alpha = 0
self.containerView.transform = CGAffineTransform(translationX: 0, y: 500)
} completion: { _ in
self.removeFromSuperview()
XSBaseAlert.Window.manager.dismissWindow()
}
}
}
extension XSBaseAlert {
class Window {
static let manager = Window()
private(set) var window: UIWindow?
func createWindow() -> UIWindow {
guard let window = window else {
let window = UIWindow(windowScene: XSTool.windowScene!)
window.backgroundColor = .clear
window.windowLevel = .alert
window.isHidden = false
self.window = window
return window
}
return window
}
func dismissWindow() {
guard let window = self.window else { return }
var isHidden = true
window.subviews.forEach {
if $0.isKind(of: XSBaseAlert.self) {
isHidden = false
}
}
if isHidden {
window.isHidden = true
self.window = nil
}
}
}
}

View File

@ -12,14 +12,16 @@ class XSDeviceId: NSObject {
static let shared = XSDeviceId()
private let key = "com.xseri.deviceid"
let id: String
lazy var id: String = {
private override init() {
if let savedID = XSKeychain.shared.read(key: key) {
return savedID
self.id = savedID
} else {
let newID = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
XSKeychain.shared.save(key: key, value: newID)
return newID
self.id = newID
}
}()
super.init()
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1A",
"green" : "0x18",
"red" : "0x00"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0x66",
"red" : "0x08"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x01",
"green" : "0x07",
"red" : "0x0C"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x0E",
"green" : "0x11",
"red" : "0x16"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x33",
"green" : "0x33",
"red" : "0x33"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x37",
"green" : "0x45",
"red" : "0x57"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 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: 722 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "签到板+飞舞的金币@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "签到板+飞舞的金币@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 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: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Wallet-three (钱包3)@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Wallet-three (钱包3)@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

View File

@ -59,6 +59,10 @@
"Expires in ## days" = "Expires in ## days";
"Coin Record" = "Coin Record";
"VIP Record" = "VIP Record";
"Order Records" = "Order Records";
"VIP Membership" = "VIP Membership";
"Auto renew,cancel anytime" = "Auto renew,cancel anytime";
"Daily Coins" = "Daily Coins";
"me_daily_1" = "Daily reward ready!";
"me_daily_2" = "Claim your rewards now.";