From f40641669803ae06c21d786617be46f9961f7280 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Thu, 13 Nov 2025 15:34:56 +0800 Subject: [PATCH] 1 --- CustomKeyboard/Network/KBNetworkManager.m | 33 +++- .../recharge_btn_bg.imageset/Contents.json | 22 +++ .../recharge_btn_bg@2x.png | Bin 0 -> 6747 bytes .../recharge_btn_bg@3x.png | Bin 0 -> 13761 bytes keyBoard/Class/Network/KBNetworkManager.m | 144 +++++++++++++++++- keyBoard/Class/Shop/V/KBShopHeadView.m | 2 + keyBoard/Class/Shop/VC/KBShopItemVC.m | 4 +- keyBoard/Class/Shop/VC/KBShopVC.m | 67 +++++++- keyBoard/KeyBoardPrefixHeader.pch | 10 ++ 9 files changed, 263 insertions(+), 19 deletions(-) create mode 100644 keyBoard/Assets.xcassets/Shop/recharge_btn_bg.imageset/Contents.json create mode 100644 keyBoard/Assets.xcassets/Shop/recharge_btn_bg.imageset/recharge_btn_bg@2x.png create mode 100644 keyBoard/Assets.xcassets/Shop/recharge_btn_bg.imageset/recharge_btn_bg@3x.png diff --git a/CustomKeyboard/Network/KBNetworkManager.m b/CustomKeyboard/Network/KBNetworkManager.m index 824109b..73b0e05 100644 --- a/CustomKeyboard/Network/KBNetworkManager.m +++ b/CustomKeyboard/Network/KBNetworkManager.m @@ -26,7 +26,8 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; if (self = [super init]) { _enabled = NO; // 键盘扩展默认无网络能力,需外部显式开启 _timeout = 10.0; - _defaultHeaders = @{ @"Accept": @"application/json" }; + // 默认接受任意类型,避免下载图片/二进制被服务端基于 Accept 拒绝 + _defaultHeaders = @{ @"Accept": @"*/*" }; // 设置基础域名,路径可相对该地址拼接 _baseURL = [NSURL URLWithString:KB_BASE_URL]; } @@ -45,10 +46,15 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; // 使用 AFHTTPRequestSerializer 生成带参数与头的请求 AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer]; serializer.timeoutInterval = self.timeout; + NSError *serror = nil; NSMutableURLRequest *req = [serializer requestWithMethod:@"GET" URLString:urlString parameters:parameters - error:NULL]; + error:&serror]; + if (serror || !req) { + if (completion) completion(nil, nil, serror ?: [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey: @"无效的URL"}]); + return nil; + } [self applyHeaders:headers toMutableRequest:req contentType:nil]; return [self startAFTaskWithRequest:req completion:completion]; } @@ -109,7 +115,11 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; // 响应先用原始数据返回,再按 Content-Type 解析 JSON(与原实现一致) self.manager.responseSerializer = [AFHTTPResponseSerializer serializer]; NSURLSessionDataTask *task = [self.manager dataTaskWithRequest:req uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { - if (error) { if (completion) completion(nil, response, error); return; } + // AFN 默认对非 2xx 的状态码返回 error;这里直接回调上层 + if (error) { + if (completion) completion(nil, response, error); + return; + } NSData *data = (NSData *)responseObject; if (![data isKindOfClass:[NSData class]]) { if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:@"无数据"}]); @@ -119,7 +129,19 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; if ([response isKindOfClass:[NSHTTPURLResponse class]]) { ct = ((NSHTTPURLResponse *)response).allHeaderFields[@"Content-Type"]; } - BOOL looksJSON = (ct && [ct.lowercaseString containsString:@"application/json"]); + // 更宽松的 JSON 判定:Content-Type 里包含 json;或首字符是 { / [ + BOOL looksJSON = (ct && [[ct lowercaseString] containsString:@"json"]); + if (!looksJSON) { + // 内容嗅探(不依赖服务端声明) + const unsigned char *bytes = data.bytes; + NSUInteger len = data.length; + for (NSUInteger i = 0; !looksJSON && i < len; i++) { + unsigned char c = bytes[i]; + if (c == ' ' || c == '\n' || c == '\r' || c == '\t') continue; + looksJSON = (c == '{' || c == '['); + break; + } + } if (looksJSON) { NSError *jsonErr = nil; id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr]; @@ -139,8 +161,7 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; if (!_manager) { NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration ephemeralSessionConfiguration]; cfg.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; - cfg.timeoutIntervalForRequest = self.timeout; - cfg.timeoutIntervalForResource = MAX(self.timeout, 30.0); + // 不在会话级别设置超时,避免与 per-request 的 serializer.timeoutInterval 产生不一致 if (@available(iOS 11.0, *)) { cfg.waitsForConnectivity = YES; } _manager = [[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:cfg]; // 默认不使用 JSON 解析器,保持原生数据,再按需解析 diff --git a/keyBoard/Assets.xcassets/Shop/recharge_btn_bg.imageset/Contents.json b/keyBoard/Assets.xcassets/Shop/recharge_btn_bg.imageset/Contents.json new file mode 100644 index 0000000..10f8506 --- /dev/null +++ b/keyBoard/Assets.xcassets/Shop/recharge_btn_bg.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "recharge_btn_bg@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "recharge_btn_bg@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/keyBoard/Assets.xcassets/Shop/recharge_btn_bg.imageset/recharge_btn_bg@2x.png b/keyBoard/Assets.xcassets/Shop/recharge_btn_bg.imageset/recharge_btn_bg@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b597c29a51dedb5cd1f423b7b3859d9c1d77f69d GIT binary patch literal 6747 zcmV-h8l>fkP)HhWao0;udowVfN6p9N0+sOqBZI}>&-N7rbyfSBWboBRQvDlXq ziNrU6e@-To&)BxT6Zkpsf51y2?9c~x$v$8VF_wO6+x8B|81N0o9Aj_){{4MkJi2U( zS=w^NIqijLg>`TW^Ug-hB_eqbl+?yLL@GD^Nt=ght2k|{-Me@9M5EC^fy%O#i%37v15mi2a1T9DvIiJLjHL_F!VqH&_%vgVv3JQO zmpsQ-})ZO{hW9fx+)vKRJtbUI% zTYxx&9d}|rXbj|ybxeW`9G8`4LqQ{Lgx}7*8W|Z`9FNC0-Ezwd4!PCL0^1O=I|0hST|#+fIwY{!ou z{~)d~TjtK4`$QxX*?wD- z+JFS}1d_%*qs$W|jfC+C7)@nC8}sVbS6}VJ)#RIV=FE8>pzx=tqe8&$|aw1B9DREns>Xm;L3gDU&G3T$N}i{73ln zOcTN@1+EzXiiMi5p>jdr58>EWh-_Y5?8U$eV9Au&gKG*G?s{O#FgU5 zxRrm51?Pam;Ac-t^3n-OUKW+)KwOfu06NMIM!N_Ww{AXWS*?O&6$F_L1evzR0)pM0 zFT3U#g0v8ay>YPzqGB(LB1X~Q51b>Oonpp5H1MOz$;s;-L+T@~5oDPrkaTu_1oN~R zA6bJH{duMd=U}{4{YxwnKZs1;yCSYQ7DU)LDv8U$ydrhuYJ^FV4DiZW9Be}%3v7Nn z)0G7#(&+;^gpD~)0YUHhr4E=kj&9sE7LG|`1;ja@F^o9Rw|BO+wQZ&*zWBv2@|`E^ zQY!@0gg#u&%{wEBr4V92CZeSo0uTVKtsNZfPa~W8NULdak*{L9 z?8O&f?Af$w(<8V|U*#TWBwSUx-jY01ksf`UAHhhm-aZRMOhLG{6k)0N$C&-)?P}?h0Ji(P$Uioi0;cD28 zJ65jLX-N#8lBrAMl59UuCpZ&csWup3%>eq7<0Ll|IOg*OZZz$T?X!{?V5}QO03$U* ztxz+_rZt4Lw|lmXJOybiaBF#G3b){nb?erBS1Gl~Ez6>~*saGT{-IHsntQ$jpULCv zJLpp1zh-g_^ex*X*E75^kjn#cZwT2uDv5a`h;{g{HA@Jtfm)$v@D-1BlxKFC46ES)%@e~O!TqV{GM94$Q%HM^Wd84vUPXsT zM@O&c$`Ow;;e<@hJ1WsdaY<&qz|VH9xP!~b;sgMLyS@UM(s$ zXHXF3A}nIgGiii2Y6$P_Z(tg5wB=(YQ=;S@(ko0Gk#KCk{q1ir>gwvc4|(H$O5zvb z&Nxu0a2S9TT7Irh5TyE4*>f}KcwTZNfMaQ9j7V&NnwS<#W(x1@@1v$%tD`ZbJw{S8 zzC(Hi9?P<2%P2N$4Lo<02? z9UXrqhAkBkSw0zdi`% zUK6au{M$0whud&_D!2uKT|E>Ym<$|2keQga`a-P+-Awn3B5M?k*@^ad?8&**P!QDG zfddB?!=AoNQjo}}7IPLZTzDHZLBzzi-nJ(%tQ9BCbO4mAJ^G7nlS34K@uSb`0i09z zlbjESRJ1Q9Nh_!;<_8A{Zy^JYC_O>SjvYICT3cI<-yPqw$LC=g*I89%2Z{wd%VWsR z1aHjbMh54U<0Cf{yfIJV`#;mUFkvTJ4oY+mM3=SWE_w6A4?mn?n^XHv%}85Yu-&&MEytZUk^FGmS&`K(4#vOn16o+G~WMD`X7yG9NSD&q92Ysv}rSu>lj)HjQyf zwj8u4jenyD+T(9r9kKEb$;Z`h%d*y{Rp?6`uqWo^8gQyp>`;9m*EmAE7#F^3VF7O@ z`oKj%wnLs-*zlX#ccz#+C{g~&NuRn>Noa4?_U+r9*CQ09y6(E`xU!n{s^hFowVsfv zE>q+6Xe<8(L~ymV0Ta_!85AFMc2R*0Ue%=m2k%x6AxH7uz| z*3Qn(Pau=EAGD*raY-s(T~Y(7OhBd1ckXjB@K~2z8#s4hGcd}H)LhxPFw{5bCys^O zD4?dYga$)fTiX?&)DHu&y1Ja1AdaY*`{x>M-xZYU+JHA%evb6TC5#8wKx4 zENkJEy7M%sckSB6<=CwM5{|2@N9S{F{*9bw!F;v5M^>9x5rV&tzucMN%M(kLJBeKB9nl)=y z>F(}s{?}T8B9RjkZ!gqo>=^5kLrg!;(PM?&5VTpBR2@Zz#_Vz_O z5{V2VowXl_ih~`5M3t^z!+`;z+hs$g{amf{HR}A1NiyqKajx)sC`jWSauO=8b%#_dP-gnP z^fTKehnT*Q(I<@n&MC)1CSsDL?pzIKP)DwUsywc-v;K}V-wg1^Om1XwE;&AOGr=1( z*+Yg5eHjXJ!k70ala+QFJIiyD>jCU*i0M1pxqQJnWgT*55L_29oIyb26AeC#r=Lvu4V9I!T#lAmSw;l+qrG@ zNh#1`C;eLUrN^~x+w`-~mP)oj#VO|()-l#CrE`@%M%+hnGt&`#Z$}Ox2+R~XNAw4o z{>fz9^=a+17o+5AqE)b7nvz+@C$u=Br>3S3XIS0LEQ!P@{SX+LC%7%fAT_ZOrZ8d$D zv>?h5%9-e5{h(dnk;{o<`_<_6FIH`%T8!K9`t|GIcgpbW`m(9YVre_u=nks%_;iJY z!1Lq-$IWtb2vyQWtrWj4w%bO5>&l$hn`?4Jw7m?P62shvzDixz@HXa|5c*v|RDa@!y`H#7vP06#NbZUoQCeDPomSngahT?pE7 zcU-HwM#Qj%hS30puI*zd;GYyTwu}yM0;I&N- zp&*3DM(!BW?uFLROcw_Wt?&qPp;)atkG6|Wh+*5aXV1U8W=De>87Bdjb?eq0z!yxe zxH6joJhmS1mB^%qfMybw+a5WDs$0P@COCiOYJz>JC0roZa-DR&TT6ot`=W5YR*s0Zdts{C&Jv5#@yCwoeXG+8ouB--?YTYUC5r<$oL%-!{H;=gw`uJhewaRVgw0 z%0zCx_12M-Cr|!khSAMLQ2$!H>p&~Mq?#Pcj?+#q4jP?NfS>(oe;L#HVL>napL=*``qW=GZbZghonjgnLT^Tu@R;8i`ac^{JkE>ZxxdX?%1Yl$Ns}Q|*W9&0)3EUkyPjz|Tw< z292pJ4az;z&rFvFtIh;d?*6V(?FTQ0HpIM*yP8sgVrq}T+~5&3P{=F`ZQNFm3=a=~ ziv|;hq~+9dX+2!-K~}2$Le5jpyFoE+7t&XA9gT~C0{qN$y&^akf1c^e;lt8)ct~1L zNsjIC;ltmercmtY3mn{YKeBo?2L=XycIM2P?Or~}OI6#*M^qagUzKf@@oLFnW&7l6 zf_(^~An00wcKc#t+RV_n7o8uJ+k%paSjms7_P418$+1yGgM)+VpUAn@vV^H~6~LaZ z+}qo`6|X}67dGR*5O+%4s^8*5kA^7A0n2Td974H4*tlGd81lDy{xPJTIcUWK&KlKr zfEth-8?|%IHP?8ns%c&Ms;6xlMrJ_ioq57DIOz==HryA_@lUi^alTk*uT<@?%~BR# zi72PD7DN=HzI>rEXSINOh?`krOLz5MB%qb0X0(}bIK zDC3cpg@jXr52gtuMV@@}$=H@HTfT}_BI94FW2J6Uu`8wR^@~+Y_^(3{*fSYa6baiS zmlHfkJ)+Wm9+1fK-&1X`cjJu%W8Va7g_`-n4}RePqgK=u;T^&>;pDaRnYpmTLc%Q) zrV07_*S|jYzylB5N!5FWK}Li2a)})Nh_vqS5^J*3j+*K7X;)a}YJ%h9`N<(v)t%)ZgyH%uE8lon@)ji%ShAaRAK^P+P?q(`$zA* z^UiztTnt;zeXi1@SE!a(hg8cj-pwQ|d<8BA8po9euZu? zoIbO4H3X>uKQmpM(0Gop0CIjPx0auoE({g~QBUXs$(*+9_>_#1Gg+MZ=_`EVQk*<&P zF`t2SA*=jjY!gk3alq+@$A5wX{@9v-{bfcIw@J`|U@% zy1F*++O_KrtTs7+{FMbELJu^82N=`4ckkZV+1dGb8#ito!I-i%P@oOfGW6x-DM%(f z_#5}9Sp

dY3WASYymF_BL3$- literal 0 HcmV?d00001 diff --git a/keyBoard/Assets.xcassets/Shop/recharge_btn_bg.imageset/recharge_btn_bg@3x.png b/keyBoard/Assets.xcassets/Shop/recharge_btn_bg.imageset/recharge_btn_bg@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f3fbaf4f26677b328f3a6e4779fb6515ad23bc0c GIT binary patch literal 13761 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 BEouM& literal 0 HcmV?d00001 diff --git a/keyBoard/Class/Network/KBNetworkManager.m b/keyBoard/Class/Network/KBNetworkManager.m index 824109b..adea2dc 100644 --- a/keyBoard/Class/Network/KBNetworkManager.m +++ b/keyBoard/Class/Network/KBNetworkManager.m @@ -15,6 +15,16 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; - (void)fail:(KBNetworkError)code completion:(KBNetworkCompletion)completion; @end +#if DEBUG +// 在使用到 Debug 辅助方法之前做前置声明,避免出现 +// “No visible @interface declares the selector …” 的编译错误。 +@interface KBNetworkManager (Debug) +- (NSString *)kb_prettyJSONStringFromObject:(id)obj; +- (NSString *)kb_textFromData:(NSData *)data; +- (NSString *)kb_trimmedString:(NSString *)s maxLength:(NSUInteger)maxLen; +@end +#endif + @implementation KBNetworkManager + (instancetype)shared { @@ -26,7 +36,8 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; if (self = [super init]) { _enabled = NO; // 键盘扩展默认无网络能力,需外部显式开启 _timeout = 10.0; - _defaultHeaders = @{ @"Accept": @"application/json" }; + // 默认接受任意类型,避免下载图片/二进制被服务端基于 Accept 拒绝 + _defaultHeaders = @{ @"Accept": @"*/*" }; // 设置基础域名,路径可相对该地址拼接 _baseURL = [NSURL URLWithString:KB_BASE_URL]; } @@ -45,11 +56,24 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; // 使用 AFHTTPRequestSerializer 生成带参数与头的请求 AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer]; serializer.timeoutInterval = self.timeout; + NSError *serror = nil; NSMutableURLRequest *req = [serializer requestWithMethod:@"GET" URLString:urlString parameters:parameters - error:NULL]; + error:&serror]; + if (serror || !req) { + if (completion) completion(nil, nil, serror ?: [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey: @"无效的URL"}]); + return nil; + } [self applyHeaders:headers toMutableRequest:req contentType:nil]; +#if DEBUG + // 打印请求信息(GET) + NSString *paramStr = [self kb_prettyJSONStringFromObject:parameters] ?: @"(null)"; + KBLOG(@"HTTP GET\nURL: %@\nHeaders: %@\n参数: %@", + req.URL.absoluteString, + req.allHTTPHeaderFields ?: @{}, + paramStr); +#endif return [self startAFTaskWithRequest:req completion:completion]; } @@ -70,6 +94,14 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; error:&error]; if (error) { if (completion) completion(nil, nil, error); return nil; } [self applyHeaders:headers toMutableRequest:req contentType:nil]; +#if DEBUG + // 打印请求信息(POST JSON) + NSString *bodyStr = [self kb_prettyJSONStringFromObject:jsonBody] ?: @"(null)"; + KBLOG(@"HTTP POST\nURL: %@\nHeaders: %@\nJSON: %@", + req.URL.absoluteString, + req.allHTTPHeaderFields ?: @{}, + bodyStr); +#endif return [self startAFTaskWithRequest:req completion:completion]; } @@ -109,9 +141,24 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; // 响应先用原始数据返回,再按 Content-Type 解析 JSON(与原实现一致) self.manager.responseSerializer = [AFHTTPResponseSerializer serializer]; NSURLSessionDataTask *task = [self.manager dataTaskWithRequest:req uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { - if (error) { if (completion) completion(nil, response, error); return; } + // AFN 默认对非 2xx 的状态码返回 error;这里先日志,再直接回调上层 + if (error) { +#if DEBUG + NSInteger status = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).statusCode : 0; + KBLOG(@"请求失败\nURL: %@\n状态: %ld\n错误: %@\nUserInfo: %@", + req.URL.absoluteString, + (long)status, + error.localizedDescription, + error.userInfo ?: @{}); +#endif + if (completion) completion(nil, response, error); + return; + } NSData *data = (NSData *)responseObject; if (![data isKindOfClass:[NSData class]]) { +#if DEBUG + KBLOG(@"无效响应\nURL: %@\n说明: %@", req.URL.absoluteString, @"未获取到数据"); +#endif if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorInvalidResponse userInfo:@{NSLocalizedDescriptionKey:@"无数据"}]); return; } @@ -119,13 +166,53 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; if ([response isKindOfClass:[NSHTTPURLResponse class]]) { ct = ((NSHTTPURLResponse *)response).allHeaderFields[@"Content-Type"]; } - BOOL looksJSON = (ct && [ct.lowercaseString containsString:@"application/json"]); + // 更宽松的 JSON 判定:Content-Type 里包含 json;或首字符是 { / [ + BOOL looksJSON = (ct && [[ct lowercaseString] containsString:@"json"]); + if (!looksJSON) { + // 内容嗅探(不依赖服务端声明) + const unsigned char *bytes = data.bytes; + NSUInteger len = data.length; + for (NSUInteger i = 0; !looksJSON && i < len; i++) { + unsigned char c = bytes[i]; + if (c == ' ' || c == '\n' || c == '\r' || c == '\t') continue; + looksJSON = (c == '{' || c == '['); + break; + } + } if (looksJSON) { NSError *jsonErr = nil; id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonErr]; - if (jsonErr) { if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDecodeFailed userInfo:@{NSLocalizedDescriptionKey:@"JSON解析失败"}]); return; } + if (jsonErr) { +#if DEBUG + KBLOG(@"响应解析失败(JSON)\nURL: %@\n错误: %@", + req.URL.absoluteString, + jsonErr.localizedDescription); +#endif + if (completion) completion(nil, response, [NSError errorWithDomain:KBNetworkErrorDomain code:KBNetworkErrorDecodeFailed userInfo:@{NSLocalizedDescriptionKey:@"JSON解析失败"}]); + return; + } +#if DEBUG + NSString *pretty = [self kb_prettyJSONStringFromObject:json] ?: @"(null)"; + pretty = [self kb_trimmedString:pretty maxLength:4096]; + NSInteger status = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).statusCode : 0; + KBLOG(@"响应成功(JSON)\nURL: %@\n状态: %ld\nContent-Type: %@\n数据: %@", + req.URL.absoluteString, + (long)status, + ct ?: @"", + pretty); +#endif if (completion) completion(json, response, nil); } else { +#if DEBUG + NSString *text = [self kb_textFromData:data]; + text = [self kb_trimmedString:text maxLength:4096]; + NSInteger status = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).statusCode : 0; + KBLOG(@"响应成功(Data)\nURL: %@\n状态: %ld\nContent-Type: %@\n数据: %@", + req.URL.absoluteString, + (long)status, + ct ?: @"", + text); +#endif if (completion) completion(data, response, nil); } }]; @@ -139,8 +226,7 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; if (!_manager) { NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration ephemeralSessionConfiguration]; cfg.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; - cfg.timeoutIntervalForRequest = self.timeout; - cfg.timeoutIntervalForResource = MAX(self.timeout, 30.0); + // 不在会话级别设置超时,避免与 per-request 的 serializer.timeoutInterval 产生不一致 if (@available(iOS 11.0, *)) { cfg.waitsForConnectivity = YES; } _manager = [[AFHTTPSessionManager alloc] initWithBaseURL:nil sessionConfiguration:cfg]; // 默认不使用 JSON 解析器,保持原生数据,再按需解析 @@ -167,3 +253,47 @@ NSErrorDomain const KBNetworkErrorDomain = @"com.company.keyboard.network"; } @end + +#pragma mark - Debug helpers + +#if DEBUG +@implementation KBNetworkManager (Debug) + +// 将对象转为漂亮的 JSON 字符串;否则返回 description +- (NSString *)kb_prettyJSONStringFromObject:(id)obj { + if (!obj || obj == (id)kCFNull) return nil; + if (![NSJSONSerialization isValidJSONObject:obj]) { + // 非标准 JSON 对象,直接 description + return [obj description]; + } + NSData *data = [NSJSONSerialization dataWithJSONObject:obj options:NSJSONWritingPrettyPrinted error:NULL]; + if (!data) return [obj description]; + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ?: [obj description]; +} + +// 尝试把二进制数据转为可读文本;失败则返回占位带长度 +- (NSString *)kb_textFromData:(NSData *)data { + if (!data) return @"(null)"; + if (data.length == 0) return @""; + // 优先 UTF-8 + NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (text.length > 0) return text; + // 再尝试常见编码 + NSArray *encs = @[@(NSASCIIStringEncoding), @(NSISOLatin1StringEncoding), @(NSUnicodeStringEncoding)]; + for (NSNumber *n in encs) { + text = [[NSString alloc] initWithData:data encoding:n.unsignedIntegerValue]; + if (text.length > 0) return text; + } + return [NSString stringWithFormat:@"", (unsigned long)data.length]; +} + +// 过长时裁剪,避免刷屏 +- (NSString *)kb_trimmedString:(NSString *)s maxLength:(NSUInteger)maxLen { + if (!s) return @""; + if (s.length <= maxLen) return s; + NSString *head = [s substringToIndex:maxLen]; + return [head stringByAppendingString:@"\n......"]; +} + +@end +#endif diff --git a/keyBoard/Class/Shop/V/KBShopHeadView.m b/keyBoard/Class/Shop/V/KBShopHeadView.m index 91d4351..d24a9fa 100644 --- a/keyBoard/Class/Shop/V/KBShopHeadView.m +++ b/keyBoard/Class/Shop/V/KBShopHeadView.m @@ -6,7 +6,9 @@ // #import "KBShopHeadView.h" +@interface KBShopHeadView() +@end @implementation KBShopHeadView - (instancetype)initWithFrame:(CGRect)frame{ diff --git a/keyBoard/Class/Shop/VC/KBShopItemVC.m b/keyBoard/Class/Shop/VC/KBShopItemVC.m index b5be936..51dbc3b 100644 --- a/keyBoard/Class/Shop/VC/KBShopItemVC.m +++ b/keyBoard/Class/Shop/VC/KBShopItemVC.m @@ -18,7 +18,7 @@ - (void)viewDidLoad { [super viewDidLoad]; - self.view.backgroundColor = [UIColor whiteColor]; + self.view.backgroundColor = [UIColor clearColor]; // 懒加载 collectionView,并添加到视图 [self.view addSubview:self.collectionView]; @@ -133,7 +133,7 @@ UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; layout.scrollDirection = UICollectionViewScrollDirectionVertical; _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; - _collectionView.backgroundColor = [UIColor whiteColor]; + _collectionView.backgroundColor = [UIColor clearColor]; _collectionView.dataSource = self; _collectionView.delegate = self; // 注册皮肤卡片 cell diff --git a/keyBoard/Class/Shop/VC/KBShopVC.m b/keyBoard/Class/Shop/VC/KBShopVC.m index d0566a9..8ad5684 100644 --- a/keyBoard/Class/Shop/VC/KBShopVC.m +++ b/keyBoard/Class/Shop/VC/KBShopVC.m @@ -13,6 +13,9 @@ #import #import #import "KBShopItemVC.h" +#import "KBSearchVC.h" +#import "MySkinVC.h" + #import "KBWebViewViewController.h" @@ -31,6 +34,10 @@ static const CGFloat JXheightForHeaderInSection = 50; @property (nonatomic, strong) JXCategoryTitleView *categoryView; @property (nonatomic, strong) NSArray *titles; @property (nonatomic, strong) UIImageView *bgImageView; // 全屏背景图 +@property (nonatomic, strong) UIButton *searchBtn; +@property (nonatomic, strong) UIButton *skinBtn; +// 记录当前分类条是否为白底,避免重复设置 +@property (nonatomic, assign) BOOL categoryIsWhite; @end @@ -45,6 +52,18 @@ static const CGFloat JXheightForHeaderInSection = 50; make.edges.equalTo(self.view); }]; [self setupUI]; + [self.view addSubview:self.skinBtn]; + [self.view addSubview:self.searchBtn]; + [self.skinBtn mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.view).offset(-16); + make.top.mas_equalTo(KB_NAV_TOTAL_HEIGHT - 25); + make.width.height.mas_equalTo(25); + }]; + [self.searchBtn mas_makeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.skinBtn.mas_left).offset(-16); + make.top.equalTo(self.skinBtn); + make.width.height.mas_equalTo(25); + }]; } @@ -62,7 +81,7 @@ static const CGFloat JXheightForHeaderInSection = 50; _userHeaderView = [[KBShopHeadView alloc] init]; _categoryView = (JXCategoryTitleView *)[[KBCategoryTitleView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, JXheightForHeaderInSection)]; self.categoryView.titles = self.titles; - self.categoryView.backgroundColor = [UIColor whiteColor]; + self.categoryView.backgroundColor = [UIColor clearColor]; self.categoryView.delegate = self; self.categoryView.titleSelectedColor = [UIColor colorWithHex:0x1B1F1A]; self.categoryView.titleColor = [UIColor colorWithHex:0x9F9F9F]; @@ -102,7 +121,7 @@ static const CGFloat JXheightForHeaderInSection = 50; [self.view addSubview:self.pagerView]; // self.pagerView.listContainerView.scrollView.scrollEnabled = false; - +// self.pagerView.listContainerView.listCellBackgroundColor = [UIColor clearColor]; self.categoryView.listContainer = (id)self.pagerView.listContainerView; //导航栏隐藏的情况,处理扣边返回,下面的代码要加上 @@ -200,10 +219,50 @@ static const CGFloat JXheightForHeaderInSection = 50; - (void)pagerView:(JXPagerView *)pagerView mainTableViewDidScroll:(UIScrollView *)scrollView { - CGFloat thresholdDistance = JXTableHeaderViewHeight; - CGFloat percent = scrollView.contentOffset.y/thresholdDistance; + // 正确的吸顶阈值:头图高度 - 固定区偏移(即导航高度) + CGFloat headerH = (CGFloat)[self tableHeaderViewHeightInPagerView:self.pagerView]; + CGFloat thresholdDistance = MAX(0.0, headerH - self.pagerView.pinSectionHeaderVerticalOffset); + CGFloat percent = (thresholdDistance > 0 ? scrollView.contentOffset.y/thresholdDistance : 1); percent = MAX(0, MIN(1, percent)); self.naviBGView.alpha = percent; + // 分类条背景:未吸顶时透明,吸顶后白色 + // 触发吸顶的大致阈值 ≈ 头图高度 - 顶部偏移(此处用头图高度近似即可) + BOOL shouldWhite = (thresholdDistance > 0.0 && scrollView.contentOffset.y >= (thresholdDistance - 0.5)); + if (shouldWhite != self.categoryIsWhite) { + self.categoryIsWhite = shouldWhite; + UIColor *bg = shouldWhite ? [UIColor whiteColor] : [UIColor clearColor]; + self.categoryView.backgroundColor = bg; + // 内部 collectionView 也同步,避免出现条纹底色 + self.categoryView.collectionView.backgroundColor = bg; + } } +#pragma mark - action +- (void)searchBtnAction{ + KBSearchVC *vc = [[KBSearchVC alloc] init]; +// [self.navigationController pushViewController:vc animated:true]; +} + +- (void)skinBtnAction{ + MySkinVC *vc = [[MySkinVC alloc] init]; +// [self.navigationController pushViewController:vc animated:true]; +} + +#pragma mark - lazy +- (UIButton *)searchBtn{ + if (!_searchBtn) { + _searchBtn = [UIButton buttonWithType:UIButtonTypeCustom]; + [_searchBtn setImage:[UIImage imageNamed:@"shop_search_icon"] forState:UIControlStateNormal]; + [_searchBtn addTarget:self action:@selector(searchBtnAction) forControlEvents:UIControlEventTouchUpInside]; + } + return _searchBtn; +} +- (UIButton *)skinBtn{ + if (!_skinBtn) { + _skinBtn = [UIButton buttonWithType:UIButtonTypeCustom]; + [_skinBtn setImage:[UIImage imageNamed:@"shop_skin_icon"] forState:UIControlStateNormal]; + [_skinBtn addTarget:self action:@selector(skinBtnAction) forControlEvents:UIControlEventTouchUpInside]; + } + return _skinBtn; +} @end diff --git a/keyBoard/KeyBoardPrefixHeader.pch b/keyBoard/KeyBoardPrefixHeader.pch index bad7123..7f61f72 100644 --- a/keyBoard/KeyBoardPrefixHeader.pch +++ b/keyBoard/KeyBoardPrefixHeader.pch @@ -41,6 +41,16 @@ //-----------------------------------------------宏定义全局----------------------------------------------------------/ +// 调试专用日志(DEBUG 打印,RELEASE 不打印)。尽量显眼,包含函数与行号。 +#if DEBUG +#define KBLOG(fmt, ...) do { \ + NSString *kb_msg__ = [NSString stringWithFormat:(fmt), ##__VA_ARGS__]; \ + NSLog(@"\n==============================[KB DEBUG]==============================\n[Function] %s\n[Line] %d\n%@\n=====================================================================\n", __PRETTY_FUNCTION__, __LINE__, kb_msg__); \ +} while(0) +#else +#define KBLOG(...) +#endif + // 通用链接(Universal Links)统一配置 // 仅需修改这里的域名/前缀,工程内所有使用 UL 的地方都会同步。 #define KB_UL_BASE @"https://app.tknb.net/ul" // 与 Associated Domains 一致