Files
18xchat/assets/Scripts/chat18x/ui/panels/ThemePanel.ts
T

699 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> | 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<AccountData>(
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<void> {
if (isPreloaded) {
logger.log("[ThemePanel] 使用预加载的主题数据");
// 直接使用预加载的数据渲染界面
const themeData = DataManager.I.getDataById<ThemeData>(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<ThemeData>(DataId.Theme);
themeData.themes = res.data;
}
}
/**
* 渲染主题数据
*/
private renderThemeData(): void {
if (this.cache.length > 0) return;
const themeData = DataManager.I.getDataById<ThemeData>(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<GirlData>(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<void> {
if (isPreloaded) {
logger.log("[ThemePanel] 使用预加载的推荐列表数据");
// 直接使用预加载的数据渲染界面
const girlData = DataManager.I.getDataById<GirlData>(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<GirlData>(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<void> {
// 防止重复加载
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<GirlData>(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<void> {
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<GirlData>(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<WalletData>(DataId.Wallet);
const balance = walletData.getOriginalBalance();
this.coinNum.string = balance.toString();
}
// 刷新 vip失效时间戳
private refreshVipExpire() {
const walletData = DataManager.I.getDataById<WalletData>(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<ShopData>(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();
};
}