import { _decorator, Component, Node, instantiate, Label, Sprite, UITransform, ScrollView, } from "cc"; import li_BaseView from "db://assets/Scripts/Main/Common/li_BaseView"; import Utils from "db://assets/Scripts/Main/Common/Utils"; import { GButton } from "db://assets/Scripts/Main/Common/GButton"; import ResManager from "db://assets/Scripts/Main/Manager/ResManager"; import { ThemeItem } from "../../uiitems/ThemeItem"; import { NavigationManager, PageTransitionType, PanelType, } from "../../manager/NavigationManager"; import LanguageUtils from "../../../Main/Common/LanguageUtils"; import { InnerMsgCode } from "../../../Main/Config/InnerMsgCode"; import { ThemeService } from "db://assets/Scripts/chat18x/network/services/ThemeService"; import { GirlService } from "db://assets/Scripts/chat18x/network/services/GirlService"; import { ShopService } from "db://assets/Scripts/chat18x/network/services/ShopService"; import { DataManager, DataId } from "../../data/DataManager"; import { ThemeData } from "../../data/ThemeData"; import { GirlData } from "../../data/GirlData"; import { WalletData } from "../../data/WalletData"; import { AccountData } from "../../data/AccountData"; import { ShopData } from "../../data/ShopData"; import { EnvData } from "../../data/EnvData"; import proto from "db://assets/Scripts/proto/proto.pb.js"; import { MainController } from "../../core/MainController"; import { logger } from "db://assets/Scripts/Main/Common/Logger"; import { RecomandItem } from "../../uiitems/RecomandItem"; import HXZ_ScrollViewList from "db://assets/Scripts/Main/Common/ScrollViewList"; import ScrollViewListItem from "db://assets/Scripts/Main/Common/ScrollviewListItem"; const { ccclass, property } = _decorator; @ccclass("ThemePanel") export class ThemePanel extends li_BaseView { private _nodeTab: any = {}; coinNum: Label; itemInst: ThemeItem; recItemInst: RecomandItem; themeContent: Node; recGirlContent: Node; private vipLeftTime: Label; //recName: Label; //recSprite: Sprite; cache: ThemeItem[] = []; // 不再需要手动管理 recItemCache // recItemCache: RecomandItem[] = []; // 虚拟列表组件 private scrollViewList: HXZ_ScrollViewList = null; // 子页面管理 // 加载状态保护 private isLoading: boolean = false; private loadingPromise: Promise | null = null; private abortController: AbortController | null = null; private recomandListBtn: Node; private moreThemeBtn: Node; private themeScroll: Node; private recomandScroll: Node; private recomandScrollView: ScrollView = null; // 推荐列表分页状态 private currentPage: number = 1; private pageLimit: number = 10; private isLoadingMore: boolean = false; private hasMoreData: boolean = true; // 记录上次触发加载的 ID,避免重复触发 private lastLoadTriggerId: number = -1; onLoadCT() { Utils.parseNode(this.node, this._nodeTab); this.coinNum = this._nodeTab.coinNum.getComponent(Label); //this.recName = this._nodeTab.characterNameText.getComponent(Label); //this.recSprite = this._nodeTab.recPic.getComponent(Sprite); this.vipLeftTime = this._nodeTab.vipText.getComponent(Label); this.recomandListBtn = this._nodeTab.recomandList; this.moreThemeBtn = this._nodeTab.moreTheme; this.themeScroll = this._nodeTab.themeScroll; this.recomandScroll = this._nodeTab.recomandScroll; // 获取推荐列表的 ScrollView 组件 if (this.recomandScroll) { this.recomandScrollView = this.recomandScroll.getComponentInChildren(ScrollView); // 获取虚拟列表组件(需要在场景中手动添加 HXZ_ScrollViewList 组件) this.scrollViewList = this.recomandScroll.getComponentInChildren(HXZ_ScrollViewList); if (!this.scrollViewList) { logger.warn( "[ThemePanel] recomandScroll 节点缺少 HXZ_ScrollViewList 组件,请在编辑器中添加" ); } } this.registerListener(); this.itemInst = this._nodeTab.ThemeItem.getComponent(ThemeItem); this.itemInst.node.active = false; this.recItemInst = this._nodeTab.recItem.getComponent(RecomandItem); // 虚拟列表会自动管理模板节点的显隐,但保留兼容性代码 this.recItemInst.node.active = false; this.themeContent = this._nodeTab.allThemecontent; this.recGirlContent = this._nodeTab.allRecGirlContent; this.Show(); } async Show(forceRefresh: boolean = false) { // 防止重复加载 if (this.isLoading) { logger.log("[ThemePanel] 已在加载中,等待当前加载完成"); if (this.loadingPromise) { await this.loadingPromise; } return; } this.isLoading = true; this.loadingPromise = this._refreshDataInternal(forceRefresh); try { await this.loadingPromise; } finally { this.isLoading = false; this.loadingPromise = null; } this.openRecomandList(); } private async _refreshDataInternal(forceRefresh: boolean = false) { // 创建新的AbortController用于取消操作 this.abortController = new AbortController(); const signal = this.abortController.signal; try { this._nodeTab.loadingRec.active = true; //some temp data this.refreshCoinNum(); this.refreshVipExpire(); const accountData = DataManager.I.getDataById( DataId.Account ); //this.uid.string = "uid: " + accountData.accId; // 检查是否已被取消 if (signal.aborted) { throw new Error("Operation was aborted"); } // 清理旧的cache节点(每次都清理,解决内存泄漏问题) this.clearCache(); // 如果需要强制刷新,先刷新数据 if (forceRefresh) { logger.log("[ThemePanel] 开始强制刷新数据"); await Promise.all([ MainController.I.refreshThemeData(), MainController.I.refreshRecommendList(), ]); logger.log("[ThemePanel] 数据刷新完成"); // 再次检查是否已被取消 if (signal.aborted) { throw new Error("Operation was aborted"); } } // 优先使用MainController中预加载的数据 const preloadStatus = MainController.I.getPreloadStatus(); logger.log("[ThemePanel] 预加载状态:", preloadStatus); // 处理主题数据 await this.handleThemeData(preloadStatus.themeData); // 检查是否已被取消 if (signal.aborted) { throw new Error("Operation was aborted"); } // 处理推荐列表数据 await this.handleRecommendData(preloadStatus.recommendList); // 检查是否已被取消 if (signal.aborted) { throw new Error("Operation was aborted"); } this._nodeTab.loadingRec.active = false; // 处理商品列表数据 await this.handleShopListData(preloadStatus.shopList); } catch (error) { logger.error("[ThemePanel] Show操作出错:", error); this._nodeTab.loadingRec.active = false; // 如果不是取消操作导致的错误,可以考虑显示错误提示 if (error.message !== "Operation was aborted") { // 这里可以添加错误提示逻辑 logger.error("[ThemePanel] 数据加载失败,请稍后重试"); } } finally { this.abortController = null; } } /** * 处理主题数据 */ private async handleThemeData(isPreloaded: boolean): Promise { if (isPreloaded) { logger.log("[ThemePanel] 使用预加载的主题数据"); // 直接使用预加载的数据渲染界面 const themeData = DataManager.I.getDataById(DataId.Theme); if (themeData && themeData.themes) { return; } } // 如果没有预加载数据,则请求数据 logger.log("[ThemePanel] 请求主题数据"); const reqData = {}; let res = await ThemeService.I.reqHallTheme(reqData); if (res && res.code === proto.cs.EnmRetCode.SUCCESS) { // 保存数据 const themeData = DataManager.I.getDataById(DataId.Theme); themeData.themes = res.data; } } /** * 渲染主题数据 */ private renderThemeData(): void { if (this.cache.length > 0) return; const themeData = DataManager.I.getDataById(DataId.Theme); const themeIds = themeData.themeIds; for (let id of themeIds) { let newNode = instantiate(this.itemInst.node); let item = newNode.getComponent(ThemeItem); item.refresh(id, this.node); newNode.active = true; this.cache.push(item); this.themeContent.addChild(newNode); } } /** * 虚拟列表渲染回调 * 该方法需要在 Cocos Creator 编辑器中配置到 HXZ_ScrollViewList 的 renderEvent * @param itemNode 虚拟列表传入的节点 * @param index 数据索引 */ onRenderRecommendItem(itemNode: Node, index: number): void { const girlData = DataManager.I.getDataById(DataId.Girl); const girlIDs = girlData.getRecommendListIds(); if (index < girlIDs.length) { const girlId = girlIDs[index]; const item = itemNode.getComponent(RecomandItem); if (!item) { logger.error( `[ThemePanel] 节点缺少 RecomandItem 组件,index: ${index}` ); return; } // 确保节点已初始化 if (!item._initialized) { item.init(); } // 刷新数据 item.refresh(girlId); itemNode.active = true; } } /** * 处理推荐数据 */ private async handleRecommendData(isPreloaded: boolean): Promise { if (isPreloaded) { logger.log("[ThemePanel] 使用预加载的推荐列表数据"); // 直接使用预加载的数据渲染界面 const girlData = DataManager.I.getDataById(DataId.Girl); if (girlData) { return; } } // 如果没有预加载数据,则请求数据 logger.log("[ThemePanel] 请求推荐列表数据"); const reqData = { page: 1, limit: 10, }; let res = await GirlService.I.reqRecommendList(reqData); if (res && res.code === proto.cs.EnmRetCode.SUCCESS) { const girlData = DataManager.I.getDataById(DataId.Girl); girlData.setRecommendList(res.data); } } /** * 渲染推荐列表数据(首次渲染) * 【已废弃】使用虚拟列表后,不再需要手动渲染,通过 numItems 触发自动渲染 */ private renderRecommendData_DEPRECATED(): void { // 该方法已被虚拟列表的自动渲染机制替代 // 保留此方法仅用于参考,实际不再调用 } /** * 滚动到底部回调(已废弃) * 该方法不再使用,改用 onRecommendScrolling() 中的滚动偏移量检测 * scroll-to-bottom 事件触发不稳定,改为在 scrolling 事件中检测距离底部的距离 */ private onRecommendScrollToBottom_DEPRECATED(): void { logger.log("[ThemePanel] 推荐列表滚动到底部"); this.loadMoreRecommendData(); } /** * 滚动中回调,基于虚拟列表 displayData 检测并触发分页加载 * 当显示到接近已加载数据末尾时(倒数第3个),自动加载下一页 */ private onRecommendScrolling(): void { if (!this.scrollViewList) return; // 获取虚拟列表的 displayData 和总数 const displayData = this.scrollViewList.displayData; const numItems = this.scrollViewList.numItems; // 检查数据有效性 if (!displayData || displayData.length === 0 || numItems === 0) return; // 获取当前显示的最后一个 item 的 ID const lastDisplayedId = displayData[displayData.length - 1].id; // 调试日志(显示当前滚动状态) const firstDisplayedId = displayData[0].id; logger.log( `[滚动] 显示范围: ID ${firstDisplayedId}-${lastDisplayedId}, 总数: ${numItems}, 已加载页: ${this.currentPage}` ); // 触发条件:显示到倒数第 3 个 item 时开始加载下一页 // 例如:总数 10,显示到 ID=7 时触发(10-3=7) // 总数 20,显示到 ID=17 时触发(20-3=17) const threshold = numItems - 3; if (lastDisplayedId >= threshold && lastDisplayedId > this.lastLoadTriggerId) { logger.log( `[ThemePanel] 接近末尾 (${lastDisplayedId}/${numItems}),触发加载第 ${ this.currentPage + 1 } 页` ); // 记录本次触发的 ID,避免重复触发 this.lastLoadTriggerId = lastDisplayedId; // 触发分页加载 this.loadMoreRecommendData(); } } /** * 加载下一页推荐数据 */ private async loadMoreRecommendData(): Promise { // 防止重复加载 if (this.isLoadingMore) { logger.log("[ThemePanel] 正在加载中,跳过"); return; } // 检查是否还有更多数据 if (!this.hasMoreData) { logger.log("[ThemePanel] 没有更多数据了"); return; } try { this.isLoadingMore = true; this.currentPage++; logger.log(`[ThemePanel] 开始加载第 ${this.currentPage} 页推荐数据`); const reqData = { page: this.currentPage, limit: this.pageLimit, }; const res = await GirlService.I.reqRecommendList(reqData); if (res && res.code === proto.cs.EnmRetCode.SUCCESS) { const girlData = DataManager.I.getDataById(DataId.Girl); // 追加数据到 GirlData girlData.addRecommendList(res.data); // 检查是否还有更多数据 const girls = res.data.girls || []; if (girls.length < this.pageLimit) { this.hasMoreData = false; logger.log("[ThemePanel] 已加载所有推荐数据"); } // 使用虚拟列表:更新总数量,自动触发渲染 if (this.scrollViewList) { const allGirlIDs = girlData.getRecommendListIds(); this.scrollViewList.numItems = allGirlIDs.length; logger.log( `[ThemePanel] 第 ${this.currentPage} 页加载完成,新增 ${girls.length} 项,总计 ${allGirlIDs.length} 项` ); } else { logger.warn("[ThemePanel] scrollViewList 未初始化,无法更新列表"); } } else { logger.warn("[ThemePanel] 加载下一页推荐数据失败:", res); // 加载失败,回退页码 this.currentPage--; } } catch (error) { logger.error("[ThemePanel] 加载下一页推荐数据异常:", error); // 加载失败,回退页码 this.currentPage--; } finally { this.isLoadingMore = false; } } /** * 渲染新增的推荐数据 * 【已废弃】使用虚拟列表后,通过更新 numItems 自动触发渲染 * @param newCount 新增数据的数量 */ private renderMoreRecommendData_DEPRECATED(newCount: number): void { // 该方法已被虚拟列表的自动渲染机制替代 // 保留此方法仅用于参考,实际不再调用 } /** * 处理商品列表数据 */ private async handleShopListData(isPreloaded: boolean): Promise { if (isPreloaded) { logger.log("[ThemePanel] 商品列表数据已预加载"); // 数据已经在DataManager中,不需要额外处理 return; } // 如果没有预加载数据,则请求数据 logger.log("[ThemePanel] 请求商品列表数据"); await this.reqShopList(); } private registerListener() { GButton.BandClick(this._nodeTab.ShopPanelEntry, this.enterShop, this); GButton.BandClick(this.recomandListBtn, this.openRecomandList, this); GButton.BandClick(this.moreThemeBtn, this.openMoreTheme, this); // 注册推荐列表滚动事件(延迟到下一帧,确保虚拟列表已初始化) this.scheduleOnce(() => { if (this.scrollViewList) { // 只监听 scrolling 事件,通过滚动偏移量检测是否接近底部 this.scrollViewList.node.on("scrolling", this.onRecommendScrolling, this); logger.log("[ThemePanel] 虚拟列表滚动事件已绑定"); } else { logger.warn( "[ThemePanel] 无法绑定滚动事件,scrollViewList 未初始化" ); } }, 0.1); Utils.addInnerEL(InnerMsgCode.LanguageChange, this, this.onLanguageChange); Utils.addInnerEL(InnerMsgCode.BalanceChange, this, this.onBalanceChange); Utils.addInnerEL( InnerMsgCode.VipExpireChange, this, this.onVipExpireChange ); } enterShop() { NavigationManager.Instance.openPopupPanel("PurchasePanel"); } openRecomandList() { this.themeScroll.active = false; this.recomandScroll.active = true; // 使用虚拟列表:设置数据总量,自动触发渲染 if (this.scrollViewList) { const girlData = DataManager.I.getDataById(DataId.Girl); const girlIDs = girlData.getRecommendListIds(); // 重置分页状态(仅在首次打开时) if (this.scrollViewList.numItems === 0) { this.currentPage = 1; this.hasMoreData = true; this.isLoadingMore = false; } this.scrollViewList.numItems = girlIDs.length; logger.log(`[ThemePanel] 打开推荐列表,共 ${girlIDs.length} 项`); } else { logger.warn("[ThemePanel] scrollViewList 未初始化"); } this.updateTagState(true); } openMoreTheme() { this.themeScroll.active = true; this.recomandScroll.active = false; //todo:是否需要刷新数据 this.renderThemeData(); this.updateTagState(false); } updateTagState(isRecomnend: boolean) { this.recomandListBtn.children[0].active = !isRecomnend; this.recomandListBtn.children[1].active = isRecomnend; this.moreThemeBtn.children[0].active = isRecomnend; this.moreThemeBtn.children[1].active = !isRecomnend; } // 刷新余额 private refreshCoinNum() { const walletData = DataManager.I.getDataById(DataId.Wallet); const balance = walletData.getOriginalBalance(); this.coinNum.string = balance.toString(); } // 刷新 vip失效时间戳 private refreshVipExpire() { const walletData = DataManager.I.getDataById(DataId.Wallet); if (!walletData.isVip) { //this.vipLeftTime.string = LanguageUtils.getText("purchasepanel.notvip"); this._nodeTab.notvipicon.active = true; this._nodeTab.notVip.active = true; this.vipLeftTime.node.active = false; } else { const vipExpire = walletData.vipExpire; const leftTime = Utils.formatVipLeftTime(vipExpire); this.vipLeftTime.node.active = true; this._nodeTab.notvipicon.active = false; this._nodeTab.notVip.active = false; if (leftTime == null) { LanguageUtils.getText("purchasepanel.notvip"); } else { this.vipLeftTime.string = leftTime; } } } // 获取商品列表 async reqShopList() { // 请求购买商品 const reqData = {}; let res = await ShopService.I.reqShopList(reqData); if (res && res.code === proto.cs.EnmRetCode.SUCCESS) { const resData = res.data; // 保存数据 const shopData = DataManager.I.getDataById(DataId.Shop); shopData.goods = res.data; } } /** * 清理缓存节点 */ private clearCache(): void { // 清理主题缓存 if (this.cache && this.cache.length > 0) { logger.log(`[ThemePanel] 清理 ${this.cache.length} 个主题缓存节点`); for (let i = this.cache.length - 1; i >= 0; i--) { if (this.cache[i] && this.cache[i].node && this.cache[i].node.isValid) { this.cache[i].node.destroy(); } } this.cache = []; } // 虚拟列表自动管理对象池,只需重置数量 if (this.scrollViewList) { this.scrollViewList.numItems = 0; logger.log("[ThemePanel] 重置虚拟列表项数量"); } // 重置推荐列表分页状态 this.currentPage = 1; this.hasMoreData = true; this.isLoadingMore = false; // 重置触发记录 this.lastLoadTriggerId = -1; } /** * 页面被隐藏时调用 */ public onHide(): void { logger.log("[ThemePanel] onHide called"); this.cancelCurrentOperation(); } /** * 取消当前操作 */ private cancelCurrentOperation(): void { if (this.abortController) { logger.log("[ThemePanel] 取消当前操作"); this.abortController.abort(); this.abortController = null; } } onDestroy(): void { logger.log("[ThemePanel] onDestroy called"); // 取消当前操作 this.cancelCurrentOperation(); // 清理缓存 this.clearCache(); // 重置加载状态 this.isLoading = false; this.loadingPromise = null; // 移除虚拟列表滚动事件监听 if (this.scrollViewList) { this.scrollViewList.node.off("scrolling", this.onRecommendScrolling, this); logger.log("[ThemePanel] 虚拟列表滚动事件已移除"); } // 移除事件监听器(修复回调函数引用问题) Utils.removeInnerEL( InnerMsgCode.LanguageChange, this, this.onLanguageChange ); Utils.removeInnerEL(InnerMsgCode.BalanceChange, this, this.onBalanceChange); Utils.removeInnerEL( InnerMsgCode.VipExpireChange, this, this.onVipExpireChange ); } // 事件回调函数(避免匿名函数导致的内存泄漏) private onLanguageChange = () => { //this.recName.string = LanguageUtils.getText(this.rectNameKey); this.refreshVipExpire(); }; private onBalanceChange = () => { this.refreshCoinNum(); }; private onVipExpireChange = () => { this.refreshVipExpire(); }; }