Files
18xchat/assets/Scripts/chat18x/manager/NavigationManager.ts
T
2025-10-14 18:54:02 +08:00

1237 lines
36 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 { ViewManager } from "db://assets/Scripts/Main/Manager/ViewManager";
import { UITransitionHelper } from "../utils/UITransitionHelper";
import GameRootUI from "../../Main/Common/GameRootUI";
import { Node, sys } from "cc";
import li_EventManager from "../../Main/Common/li_EventManager";
import { InnerMsgCode } from "../../Main/Config/InnerMsgCode";
import { ThemePanel } from "../ui/panels/ThemePanel";
import { GirlListPanel } from "../ui/panels/GirlListPanel";
import li_BaseView from "../../Main/Common/li_BaseView";
import { logger } from "db://assets/Scripts/Main/Common/Logger";
/**
* 页面过渡动画类型枚举
*/
export enum PageTransitionType {
NONE = "none",
SLIDE_LEFT = "slide_left",
SLIDE_RIGHT = "slide_right",
SLIDE_UP = "slide_up",
SLIDE_DOWN = "slide_down",
FADE = "fade",
SCALE = "scale",
}
/**
* NavigationPanel面板类型枚举
*/
export enum PanelType {
THEME = "ThemePanel",
RANK = "RankPanel",
PAST_GIRL_LIST = "PastGirlListPanel",
PERSONAL = "PersonalPanel",
}
/**
* 页面过渡动画配置接口
*/
export interface PageTransitionConfig {
incomingTransition: PageTransitionType;
outgoingTransition: PageTransitionType;
duration?: number;
simultaneous?: boolean;
}
/**
* 子面板动画配置接口
*/
export interface SubPanelAnimationConfig {
openAnimation: PageTransitionType;
closeAnimation: PageTransitionType;
duration?: number;
hideBasePanel?: boolean; // 是否隐藏basePanel,用于全屏子面板
}
/**
* 导航管理器
*
* 负责管理游戏中的页面导航和路由,将导航逻辑从业务逻辑中分离出来
* 统一管理NavigationPanel和其他面板的切换
*
* @author AI Chat System
* @version 1.0.0
*/
export class NavigationManager {
private static _instance: NavigationManager;
// ==================== 主面板管理相关 ====================
private navigationPanel: any = null; // NavigationPanel实例引用
private currentActivePanelType: PanelType = PanelType.PAST_GIRL_LIST;
private currentActivePanel: Node = null;
private panelCache: Map<PanelType, Node> = new Map();
// ==================== 子面板栈管理相关 ====================
private subPanelStacks: Map<PanelType, Node[]> = new Map();
private subPanelMapping: Map<Node, PanelType> = new Map();
private subPanelAnimationConfigs: Map<Node, SubPanelAnimationConfig> = new Map();
private hiddenBasePanels: Map<Node, Node> = new Map();
private subPanelHiddenStates: Map<Node, Map<Node, boolean>> = new Map();
private tempHiddenStates: Map<Node, boolean> = null;
// ==================== 弹窗管理相关 ====================
private popupPanels: Set<Node> = new Set();
// ==================== 角色选择相关 ====================
private selectedGirlId: number = 10002;
private selectedCategoryId: number = -1;
private static LastSelectGirlID = "LastSelectGirlID";
// ==================== 动画状态 ====================
private isAnimating: boolean = false;
/**
* 获取NavigationManager的单例实例
* @returns 导航管理器实例
* @static
*/
public static get Instance(): NavigationManager {
if (!this._instance) {
this._instance = new NavigationManager();
//初始化默认选的girl
let cacheGirlId = sys.localStorage.getItem(this.LastSelectGirlID);
if (!cacheGirlId) {
cacheGirlId = 10016;
sys.localStorage.setItem(this.LastSelectGirlID, cacheGirlId);
}
this._instance.selectedGirlId = Number(cacheGirlId);
let cacheCategoryId = sys.localStorage.getItem("LastSelectCategoryId");
if (!cacheCategoryId) {
cacheCategoryId = 0;
sys.localStorage.setItem("LastSelectCategoryId", cacheCategoryId);
}
this._instance.selectedCategoryId = cacheCategoryId;
}
return this._instance;
}
private constructor() {
// 私有构造函数,确保单例模式
}
// ==================== 公共方法 ====================
/**
* 注册NavigationPanel实例
* @param panel NavigationPanel实例
*/
public registerNavigationPanel(panel: any): void {
this.navigationPanel = panel;
logger.log("[NavigationManager] NavigationPanel registered");
}
/**
* 统一的面板切换方法
* @param panelType 目标面板类型
* @param force 是否强制切换(即使已经在当前面板)
* @param callback 切换完成后的回调函数
* @returns 返回切换后的面板节点,失败返回null
*/
public switchToPanel(
panelType: PanelType,
force: boolean = false,
callback?: (panel: Node) => void
): Node | null {
if (!panelType) {
logger.error("[NavigationManager] switchToPanel: panelType is invalid");
return null;
}
if (this.currentActivePanelType === panelType && !force) {
logger.log(
`[NavigationManager] Already on ${panelType}, skipping switch`
);
callback && callback(this.getCurrentActivePanelNode());
return this.currentActivePanel;
}
if (!this.navigationPanel) {
logger.warn(
"[NavigationManager] NavigationPanel not registered"
);
return null;
}
// 切换面板前先关闭所有弹窗
this.closeAllPopups();
// 通知NavigationPanel显示加载状态
this.navigationPanel.showLoading?.();
// 计算动画方向
const currentIndex = this.getPanelIndex(this.currentActivePanelType);
const targetIndex = this.getPanelIndex(panelType);
const isMovingRight = currentIndex < targetIndex;
const hideDirection = isMovingRight ? "left" : "right";
const showDirection = isMovingRight ? "right" : "left";
// 清空当前主面板的子页面栈
if (this.currentActivePanelType) {
logger.log(
`[NavigationManager] 切换主面板,清空当前面板的子页面栈: ${this.currentActivePanelType}`
);
this.clearSubPanelStack(this.currentActivePanelType, true);
}
// 隐藏当前面板
if (this.currentActivePanel && this.currentActivePanel.isValid) {
this.hidePanelWithAnimation(this.currentActivePanel, hideDirection);
}
// 加载并显示目标面板
this.loadOrGetPanel(panelType, (panel: Node) => {
this.currentActivePanelType = panelType;
// 更新NavigationPanel按钮状态
this.navigationPanel.updateButtonStates?.(panelType);
// 显示面板
this.showPanelWithAnimation(panel, showDirection, () => {
this.currentActivePanel = panel;
this.navigationPanel.hideLoading?.();
if (callback instanceof Function) callback(panel);
});
});
}
// ==================== 子面板管理方法 ====================
/**
* 打开子面板
* @param panelName 子面板名称
* @param basePanel 基础面板节点
* @param data 传递给子面板的数据
* @param animationConfig 动画配置
*/
public openSubPanel(
panelName: string,
basePanel: Node,
data: any = null,
animationConfig?: SubPanelAnimationConfig
): void {
if (!panelName) {
logger.error("[NavigationManager] openSubPanel: panelName is empty");
return;
}
if (!basePanel || !basePanel.isValid) {
logger.error("[NavigationManager] openSubPanel: basePanel is invalid");
return;
}
// 确定主面板类型
const basePanelType = this.getPanelType(basePanel);
if (!basePanelType) {
logger.error(
`[NavigationManager] 无法确定basePanel类型: ${basePanel.name}`
);
return;
}
// 使用默认动画配置
const defaultConfig: SubPanelAnimationConfig = {
openAnimation: PageTransitionType.SCALE,
closeAnimation: PageTransitionType.SCALE,
duration: 0.3,
hideBasePanel: false,
};
const config = animationConfig || defaultConfig;
// 获取当前栈顶的子面板
const currentTopSubPanel = this.getTopSubPanel(basePanelType);
// 如果需要隐藏basePanel,同时隐藏栈中所有子面板,并记录它们的原始显隐状态
if (config.hideBasePanel) {
logger.log(
`[NavigationManager] hideBasePanel=true,隐藏basePanel并记录栈中所有子面板的显隐状态`
);
// 创建临时存储Map来记录所有被隐藏子面板的原始active状态
this.tempHiddenStates = new Map<Node, boolean>();
// 先隐藏当前显示的栈顶子面板(如果有)
if (currentTopSubPanel && currentTopSubPanel.isValid) {
// 记录当前栈顶的原始状态
this.tempHiddenStates.set(
currentTopSubPanel,
currentTopSubPanel.active
);
logger.log(
`[NavigationManager] 记录当前栈顶子面板状态: ${currentTopSubPanel.name}, 原始active=${currentTopSubPanel.active}`
);
// 隐藏当前栈顶
if (currentTopSubPanel.active) {
currentTopSubPanel.active = false;
logger.log(
`[NavigationManager] 隐藏当前栈顶子面板: ${currentTopSubPanel.name}`
);
}
}
// 获取当前栈中的所有子面板
const stack = this.subPanelStacks.get(basePanelType);
if (stack && stack.length > 0) {
// 遍历栈中所有子面板,记录并隐藏它们
for (const subPanel of stack) {
// 跳过已经处理的当前栈顶
if (subPanel === currentTopSubPanel) {
continue;
}
if (subPanel && subPanel.isValid) {
// 记录原始active状态
this.tempHiddenStates.set(subPanel, subPanel.active);
logger.log(
`[NavigationManager] 记录子面板状态: ${subPanel.name}, 原始active=${subPanel.active}`
);
// 隐藏子面板
if (subPanel.active) {
subPanel.active = false;
logger.log(
`[NavigationManager] 隐藏栈中子面板: ${subPanel.name}`
);
}
}
}
}
// 同时记录basePanel的原始active状态
this.tempHiddenStates.set(basePanel, basePanel.active);
logger.log(
`[NavigationManager] 记录basePanel状态: ${basePanel.name}, 原始active=${basePanel.active}`
);
// 隐藏basePanel
if (basePanel.active) {
logger.log(`[NavigationManager] 隐藏basePanel: ${basePanel.name}`);
basePanel.active = false;
}
} else {
// 不需要隐藏basePanel,只隐藏当前栈顶子面板(原有逻辑)
if (currentTopSubPanel && currentTopSubPanel.isValid) {
logger.log(
`[NavigationManager] 隐藏当前栈顶子面板: ${currentTopSubPanel.name}`
);
this.hideSubPanel(currentTopSubPanel);
}
}
const callback = (n: Node) => {
if (!n || !n.isValid) {
logger.error(
"[NavigationManager] openSubPanel: created panel is invalid"
);
return;
}
// 初始化栈(如果不存在)
if (!this.subPanelStacks.has(basePanelType)) {
this.subPanelStacks.set(basePanelType, []);
}
// 将新子面板推入栈顶
const stack = this.subPanelStacks.get(basePanelType);
stack.push(n);
// 记录子面板到主面板的映射
this.subPanelMapping.set(n, basePanelType);
// 存储动画配置
this.subPanelAnimationConfigs.set(n, config);
// 如果配置了隐藏basePanel,记录映射关系
if (config.hideBasePanel) {
this.hiddenBasePanels.set(n, basePanel);
logger.log(
`[NavigationManager] 记录隐藏映射: ${n.name} -> ${basePanel.name}`
);
// 如果有临时存储的隐藏状态,转移到新面板的映射中
if (this.tempHiddenStates && this.tempHiddenStates.size > 0) {
this.subPanelHiddenStates.set(n, this.tempHiddenStates);
logger.log(
`[NavigationManager] 保存${this.tempHiddenStates.size}个子面板的隐藏状态到${n.name}`
);
// 清空临时存储
this.tempHiddenStates = null;
}
}
logger.log(
`[NavigationManager] 子面板已推入栈顶: ${n.name}, 当前栈深度: ${stack.length}`
);
// 执行打开动画
this.playSubPanelOpenAnimation(n, config);
// 动画完成后,根据栈顶子面板更新 basePanel 显隐状态
this.updateBasePanelVisibility(basePanelType);
};
ViewManager.I.openBundlesView(panelName, data, callback);
}
/**
* 将过渡类型转换为方向字符串
*/
private getDirectionFromTransitionType(
transitionType: PageTransitionType
): "left" | "right" | "up" | "down" {
switch (transitionType) {
case PageTransitionType.SLIDE_LEFT:
return "left";
case PageTransitionType.SLIDE_RIGHT:
return "right";
case PageTransitionType.SLIDE_UP:
return "up";
case PageTransitionType.SLIDE_DOWN:
return "down";
default:
return "left"; // 默认向左
}
}
/**
* 执行子面板打开动画
*/
private playSubPanelOpenAnimation(
panel: Node,
config: SubPanelAnimationConfig
): void {
if (!panel || !panel.isValid) {
return;
}
const duration = config.duration || 0.3;
switch (config.openAnimation) {
case PageTransitionType.SCALE:
UITransitionHelper.scaleIn(panel, duration);
break;
case PageTransitionType.FADE:
UITransitionHelper.fadeIn(panel, duration);
break;
case PageTransitionType.SLIDE_UP:
UITransitionHelper.slideInFromBottom(panel, duration);
break;
case PageTransitionType.SLIDE_DOWN:
UITransitionHelper.slideInFromTop(panel, duration);
break;
case PageTransitionType.SLIDE_LEFT:
UITransitionHelper.slideInFromRight(panel, duration);
break;
case PageTransitionType.SLIDE_RIGHT:
UITransitionHelper.slideInFromLeft(panel, duration);
break;
default:
// 无动画,直接显示
break;
}
}
/**
* 执行子面板关闭动画
*/
private playSubPanelCloseAnimation(
panel: Node,
config: SubPanelAnimationConfig,
callback?: Function
): void {
if (!panel || !panel.isValid) {
callback && callback();
return;
}
const duration = config.duration || 0.3;
switch (config.closeAnimation) {
case PageTransitionType.SCALE:
UITransitionHelper.scaleOut(panel, duration, callback);
break;
case PageTransitionType.FADE:
UITransitionHelper.fadeOut(panel, duration, callback);
break;
case PageTransitionType.SLIDE_UP:
UITransitionHelper.slideOutToTop(panel, duration, callback);
break;
case PageTransitionType.SLIDE_DOWN:
UITransitionHelper.slideOutToBottom(panel, duration, callback);
break;
case PageTransitionType.SLIDE_LEFT:
UITransitionHelper.slideOutToLeft(panel, duration, callback);
break;
case PageTransitionType.SLIDE_RIGHT:
UITransitionHelper.slideOutToRight(panel, duration, callback);
break;
default:
// 无动画,直接执行回调
callback && callback();
break;
}
}
// ==================== 弹窗管理方法 ====================
/**
* 打开弹窗面板
* @param panelName 弹窗面板名称
* @param data 传递的数据
* @param callback 可选的回调函数
*/
public openPopupPanel(
panelName: string,
data: any = null,
callback?: Function
): void {
if (!panelName) {
logger.error("[NavigationManager] openPopupPanel: panelName is empty");
return;
}
const popupCallback = (panel: Node) => {
if (!panel || !panel.isValid) {
logger.error(
"[NavigationManager] openPopupPanel: created panel is invalid"
);
return;
}
// 将弹窗添加到管理集合中
this.popupPanels.add(panel);
logger.log(
`[NavigationManager] Popup panel opened: ${panelName}, total popups: ${this.popupPanels.size}`
);
if (callback) {
callback(panel);
}
};
ViewManager.I.openBundlesPopupView(panelName, data, popupCallback);
}
/**
* 关闭弹窗面板
* @param panel 要关闭的弹窗节点
*/
public closePopupPanel(panel: Node): void {
if (!panel || !panel.isValid) {
logger.warn("[NavigationManager] closePopupPanel: panel is invalid");
return;
}
// 从弹窗管理集合中移除
if (this.popupPanels.has(panel)) {
this.popupPanels.delete(panel);
logger.log(
`[NavigationManager] Popup panel closed: ${panel.name}, total popups: ${this.popupPanels.size}`
);
}
// 关闭面板
try {
const baseView = panel.getComponent(li_BaseView);
if (baseView) {
baseView.close();
} else {
panel.destroy();
}
} catch (error) {
logger.error("[NavigationManager] closePopupPanel error:", error);
}
}
/**
* 关闭所有弹窗
*/
private closeAllPopups(): void {
if (this.popupPanels.size === 0) {
return;
}
logger.log(
`[NavigationManager] Closing all popups, count: ${this.popupPanels.size}`
);
// 复制Set以避免在迭代过程中修改
const popupPanelsToClose = Array.from(this.popupPanels);
for (const panel of popupPanelsToClose) {
if (panel && panel.isValid) {
this.closePopupPanel(panel);
}
}
// 确保清空集合
this.popupPanels.clear();
}
/**
* 获取指定主面板栈顶的子面板
* @param panelType 主面板类型
* @returns 栈顶子面板,如果栈为空则返回null
*/
private getTopSubPanel(panelType: PanelType): Node | null {
const stack = this.subPanelStacks.get(panelType);
if (!stack || stack.length === 0) {
return null;
}
return stack[stack.length - 1];
}
/**
* 清空指定主面板的子页面栈
* @param panelType 主面板类型
* @param withAnimation 是否播放动画
* @param callback 清空完成后的回调
*/
private clearSubPanelStack(
panelType: PanelType,
withAnimation: boolean = true,
callback?: Function
): void {
const stack = this.subPanelStacks.get(panelType);
if (!stack || stack.length === 0) {
callback && callback();
return;
}
if (!withAnimation) {
// 不播放动画,直接销毁所有子面板
for (let i = stack.length - 1; i >= 0; i--) {
const subPanel = stack[i];
if (subPanel && subPanel.isValid) {
// 清理映射
this.subPanelMapping.delete(subPanel);
this.subPanelAnimationConfigs.delete(subPanel);
this.hiddenBasePanels.delete(subPanel);
// 销毁节点
const baseView = subPanel.getComponent(li_BaseView);
if (baseView) {
baseView.close();
} else {
subPanel.destroy();
}
}
}
this.subPanelStacks.delete(panelType);
callback && callback();
return;
}
// 播放动画
let completedCount = 0;
const totalCount = stack.length;
const onAnimationComplete = () => {
completedCount++;
if (completedCount >= totalCount) {
this.subPanelStacks.delete(panelType);
callback && callback();
}
};
// 同时播放所有子面板的关闭动画
for (let i = stack.length - 1; i >= 0; i--) {
const subPanel = stack[i];
if (subPanel && subPanel.isValid) {
// 获取动画配置
let config = this.subPanelAnimationConfigs.get(subPanel);
if (!config) {
config = {
openAnimation: PageTransitionType.SCALE,
closeAnimation: PageTransitionType.SCALE,
duration: 0.2,
};
}
// 播放关闭动画
this.playSubPanelCloseAnimation(subPanel, config, () => {
// 清理映射
this.subPanelMapping.delete(subPanel);
this.subPanelAnimationConfigs.delete(subPanel);
this.hiddenBasePanels.delete(subPanel);
// 销毁节点
const baseView = subPanel.getComponent(li_BaseView);
if (baseView) {
baseView.close();
} else {
subPanel.destroy();
}
onAnimationComplete();
});
} else {
onAnimationComplete();
}
}
}
/**
* 显示子面板
* @param panel 子面板节点
* @param config 动画配置(可选)
* @param withAnimation 是否播放打开动画,默认为 true
*/
private showSubPanel(
panel: Node,
config?: SubPanelAnimationConfig,
withAnimation: boolean = true
): void {
if (!panel || !panel.isValid) {
return;
}
panel.active = true;
if (config && withAnimation) {
this.playSubPanelOpenAnimation(panel, config);
}
}
/**
* 隐藏子面板(不销毁)
* @param panel 子面板节点
* @param callback 隐藏完成后的回调
*/
private hideSubPanel(panel: Node, callback?: Function): void {
if (!panel || !panel.isValid) {
callback && callback();
return;
}
// 直接隐藏,不播放动画(因为会被新面板覆盖)
panel.active = false;
callback && callback();
}
/**
* 恢复被隐藏的子面板的原始显隐状态
* @param panelNode 当前要关闭的子面板
* @param newTopSubPanel 新的栈顶子面板(可选,保留参数以保持接口兼容性)
* @private
*/
private restoreHiddenSubPanels(
panelNode: Node,
newTopSubPanel: Node | null
): void {
if (!this.subPanelHiddenStates.has(panelNode)) {
return;
}
const hiddenStates = this.subPanelHiddenStates.get(panelNode);
logger.log(
`[NavigationManager] 恢复${hiddenStates.size}个子面板的原始显隐状态`
);
// 遍历恢复每个子面板的原始active状态
// 注意:basePanel 的显隐状态现在统一由 updateBasePanelVisibility() 管理
// 这里只恢复栈中其他子面板的状态
hiddenStates.forEach((originalActive, subPanel) => {
if (subPanel && subPanel.isValid) {
subPanel.active = originalActive;
logger.log(
`[NavigationManager] 恢复子面板: ${subPanel.name}, active=${originalActive}`
);
}
});
// 清理隐藏状态映射
this.subPanelHiddenStates.delete(panelNode);
}
/**
* 执行子面板关闭的降级处理(当面板不在栈中或栈已空时)
* @private
*/
private handleSubPanelCloseFallback(
panelNode: Node,
panel: Node | li_BaseView,
basePanelType: PanelType,
animationConfig?: SubPanelAnimationConfig
): void {
// 获取动画配置
let config =
animationConfig || this.subPanelAnimationConfigs.get(panelNode);
if (!config) {
config = {
openAnimation: PageTransitionType.SCALE,
closeAnimation: PageTransitionType.SCALE,
duration: 0.3,
hideBasePanel: false,
};
}
// 播放关闭动画并销毁
this.playSubPanelCloseAnimation(panelNode, config, () => {
// 恢复被隐藏的子面板状态(如果有)
this.restoreHiddenSubPanels(panelNode, null);
// 清理映射
this.subPanelMapping.delete(panelNode);
this.subPanelAnimationConfigs.delete(panelNode);
this.hiddenBasePanels.delete(panelNode);
try {
if (panel instanceof Node) {
panel.destroy();
} else {
panel.close();
}
} catch (error) {
logger.error("[NavigationManager] closeSubPanel error:", error);
}
// 统一使用 updateBasePanelVisibility 更新 basePanel 显隐状态
this.updateBasePanelVisibility(basePanelType);
});
}
/**
* 关闭子面板
* @param panel 要关闭的子面板节点或BaseView组件
* @param animationConfig 动画配置(可选)
*/
public closeSubPanel(
panel: Node | li_BaseView,
animationConfig?: SubPanelAnimationConfig
): void {
if (!panel) {
logger.warn(
"[NavigationManager] closeSubPanel: panel is null or undefined"
);
return;
}
let panelNode: Node;
if (panel instanceof Node) {
panelNode = panel;
} else {
panelNode = panel.node;
}
if (!panelNode || !panelNode.isValid) {
logger.warn("[NavigationManager] closeSubPanel: panelNode is invalid");
return;
}
// 确定子面板所属的主面板类型
const basePanelType = this.subPanelMapping.get(panelNode);
if (!basePanelType) {
logger.warn(
`[NavigationManager] closeSubPanel: 无法确定子面板所属的主面板类型: ${panelNode.name}`
);
return;
}
// 获取栈
const stack = this.subPanelStacks.get(basePanelType);
if (!stack || stack.length === 0) {
logger.warn(
`[NavigationManager] closeSubPanel: 栈为空或不存在,执行降级处理: ${panelNode.name}`
);
this.handleSubPanelCloseFallback(panelNode, panel, basePanelType, animationConfig);
return;
}
// 查找子面板在栈中的索引
const panelIndex = stack.indexOf(panelNode);
if (panelIndex === -1) {
logger.warn(
`[NavigationManager] closeSubPanel: 子面板不在栈中,执行降级处理: ${panelNode.name}`
);
this.handleSubPanelCloseFallback(panelNode, panel, basePanelType, animationConfig);
return;
}
// 从栈中移除
stack.splice(panelIndex, 1);
logger.log(
`[NavigationManager] 从栈中移除子面板: ${panelNode.name}, 剩余栈深度: ${stack.length}`
);
// 获取动画配置(优先使用传入的配置,然后使用存储的配置,最后使用默认配置)
let config = animationConfig;
if (!config && this.subPanelAnimationConfigs.has(panelNode)) {
config = this.subPanelAnimationConfigs.get(panelNode);
}
if (!config) {
config = {
openAnimation: PageTransitionType.SCALE,
closeAnimation: PageTransitionType.SCALE,
duration: 0.3,
hideBasePanel: false,
};
}
// 检查是否需要恢复显示basePanel或显示下一层子面板
const shouldRestoreBasePanel =
config.hideBasePanel && this.hiddenBasePanels.has(panelNode);
const basePanel = shouldRestoreBasePanel
? this.hiddenBasePanels.get(panelNode)
: null;
// 获取新的栈顶子面板(如果有)
const newTopSubPanel = this.getTopSubPanel(basePanelType);
// 执行关闭动画
this.playSubPanelCloseAnimation(panelNode, config, () => {
// 恢复被隐藏的子面板状态(如果有)
this.restoreHiddenSubPanels(panelNode, newTopSubPanel);
// 清理映射
this.subPanelMapping.delete(panelNode);
this.subPanelAnimationConfigs.delete(panelNode);
if (this.hiddenBasePanels.has(panelNode)) {
this.hiddenBasePanels.delete(panelNode);
}
// 销毁或关闭panel
try {
if (panel instanceof Node) {
panel.destroy();
} else {
panel.close();
}
} catch (error) {
logger.error("[NavigationManager] closeSubPanel error:", error);
}
// 关闭动画完成后,决定显示什么
if (newTopSubPanel && newTopSubPanel.isValid) {
// 栈中还有子面板,显示新的栈顶(不播放动画,直接显示)
logger.log(
`[NavigationManager] 显示新的栈顶子面板(无动画): ${newTopSubPanel.name}`
);
const newTopConfig = this.subPanelAnimationConfigs.get(newTopSubPanel);
this.showSubPanel(newTopSubPanel, newTopConfig, false); // 传入 false,不播放动画
}
// 统一使用 updateBasePanelVisibility 更新 basePanel 显隐状态
// 这个方法会根据当前栈顶是否是特殊面板自动决定显示或隐藏
this.updateBasePanelVisibility(basePanelType);
// 如果栈完全清空,删除映射
if (stack.length === 0) {
this.subPanelStacks.delete(basePanelType);
logger.log(
`[NavigationManager] 子页面栈已清空,删除映射: ${basePanelType}`
);
}
});
}
// ==================== 私有辅助方法 ====================
/**
* 检测是否是特殊面板(GirlDetailPanel 或 ChatPanel
* 这些特殊面板在显示时需要隐藏对应的主页面
* @param panel 要检测的面板节点
* @returns 是否是特殊面板
* @private
*/
private isSpecialPanel(panel: Node): boolean {
if (!panel || !panel.isValid) {
return false;
}
return panel.name === "GirlDetailPanel" || panel.name === "ChatPanel";
}
/**
* 根据栈顶子面板更新 basePanel 的显隐状态
* 如果栈顶是特殊面板(GirlDetailPanel 或 ChatPanel),则隐藏 basePanel
* 否则显示 basePanel
* @param basePanelType 主面板类型
* @private
*/
private updateBasePanelVisibility(basePanelType: PanelType): void {
const basePanel = this.panelCache.get(basePanelType);
if (!basePanel || !basePanel.isValid) {
return;
}
const topSubPanel = this.getTopSubPanel(basePanelType);
if (topSubPanel && topSubPanel.isValid && this.isSpecialPanel(topSubPanel)) {
// 栈顶是特殊面板,隐藏 basePanel
if (basePanel.active) {
logger.log(
`[NavigationManager] 隐藏 basePanel(栈顶是特殊面板 ${topSubPanel.name}: ${basePanel.name}`
);
basePanel.active = false;
}
} else {
// 栈顶不是特殊面板或栈为空,显示 basePanel
if (!basePanel.active) {
logger.log(
`[NavigationManager] 显示 basePanel(栈顶不是特殊面板): ${basePanel.name}`
);
basePanel.active = true;
}
}
}
/**
* 获取面板索引(用于动画方向计算)
* @param panelType 面板类型
* @returns 面板索引
*/
private getPanelIndex(panelType: PanelType): number {
switch (panelType) {
case PanelType.THEME:
return 0;
case PanelType.RANK:
return 1;
case PanelType.PAST_GIRL_LIST:
return 2;
case PanelType.PERSONAL:
return 3;
default:
return 0;
}
}
/**
* 根据节点名称获取面板类型
* @param node 节点
* @returns 面板类型,如果无法识别则返回null
*/
private getPanelType(node: Node): PanelType | null {
if (!node || !node.isValid) {
logger.warn("[NavigationManager] getPanelType: node is invalid");
return null;
}
switch (node.name) {
case "ThemePanel":
return PanelType.THEME;
case "RankPanel":
return PanelType.RANK;
case "PastGirlListPanel":
return PanelType.PAST_GIRL_LIST;
case "PersonalPanel":
return PanelType.PERSONAL;
default:
logger.warn(
`[NavigationManager] Unknown panel type for node: ${node.name}`
);
return null;
}
}
/**
* 获取面板名称
*/
private getPanelName(panelType: PanelType): string {
return panelType as string;
}
/**
* 获取面板数据(预留用于扩展)
* @param panelType 面板类型
* @returns 面板数据,当前返回null
*/
private getPanelData(panelType: PanelType): any {
// 所有主页面都不需要特殊数据,此方法预留用于未来扩展
return null;
}
/**
* 加载或获取面板(从缓存或新建)
* @param panelType 面板类型
* @param callback 面板加载完成的回调函数
*/
private loadOrGetPanel(
panelType: PanelType,
callback: (panel: Node) => void
): void {
// 检查缓存
if (this.panelCache.has(panelType)) {
const cachedPanel = this.panelCache.get(panelType);
if (cachedPanel && cachedPanel.isValid) {
// 注意:子面板的清理已经在 switchToPanel 中通过 clearSubPanelStack 完成了
// 这里不需要再清理
// 特殊处理:主题面板刷新(避免重复调用Show)
if (panelType === PanelType.THEME) {
const themePanel = cachedPanel.getComponent(ThemePanel);
if (themePanel) {
// 不在这里调用Show(),因为ThemePanel已经有加载状态保护
// 只在必要时调用onHide()确保子页面状态正确
if (typeof themePanel.onHide === "function") {
logger.log("[NavigationManager] 确保主题面板子页面状态正确");
themePanel.onHide();
}
}
}
callback(cachedPanel);
return;
} else {
this.panelCache.delete(panelType);
}
}
// 加载新面板
const panelName = this.getPanelName(panelType);
const openData = this.getPanelData(panelType);
ViewManager.I.openBundlesView(panelName, openData, (panel: Node) => {
if (panel && panel.isValid) {
this.panelCache.set(panelType, panel);
callback(panel);
}
});
}
/**
* 隐藏面板并播放动画
* @param panel 要隐藏的面板节点
* @param direction 动画方向
* @param callback 动画完成的回调函数
*/
private hidePanelWithAnimation(
panel: Node,
direction: "left" | "right" | "up" | "down",
callback?: () => void
): void {
if (!panel || !panel.isValid) {
callback && callback();
return;
}
// 注意:子面板的清理已经在 switchToPanel 中通过 clearSubPanelStack 完成了
// 这里只需要播放主面板的隐藏动画
if (!panel.active) panel.active = true;
const animationCallback = () => {
panel.active = false;
callback && callback();
};
// 主面板动画
switch (direction) {
case "left":
UITransitionHelper.slideOutToLeft(panel, 0.3, animationCallback);
break;
case "right":
UITransitionHelper.slideOutToRight(panel, 0.3, animationCallback);
break;
case "up":
UITransitionHelper.slideOutToTop(panel, 0.3, animationCallback);
break;
case "down":
UITransitionHelper.slideOutToBottom(panel, 0.3, animationCallback);
break;
default:
// 默认左侧滑出
UITransitionHelper.slideOutToLeft(panel, 0.3, animationCallback);
break;
}
}
/**
* 显示面板并播放动画
* @param panel 要显示的面板节点
* @param direction 动画方向
* @param callback 动画完成的回调函数
*/
private showPanelWithAnimation(
panel: Node,
direction: "left" | "right" | "up" | "down",
callback?: () => void
): void {
if (!panel || !panel.isValid) {
callback && callback();
return;
}
panel.active = true;
switch (direction) {
case "left":
UITransitionHelper.slideInFromLeft(panel, 0.3, callback);
break;
case "right":
UITransitionHelper.slideInFromRight(panel, 0.3, callback);
break;
case "up":
UITransitionHelper.slideInFromTop(panel, 0.3, callback);
break;
case "down":
UITransitionHelper.slideInFromBottom(panel, 0.3, callback);
break;
default:
// 默认从右侧滑入
UITransitionHelper.slideInFromRight(panel, 0.3, callback);
break;
}
}
// ==================== 公共访问器方法 ====================
/**
* 获取当前激活的面板类型
* @returns 当前激活的面板类型
*/
public getCurrentActivePanel(): PanelType {
return this.currentActivePanelType;
}
/**
* 获取当前激活的面板节点
*/
public getCurrentActivePanelNode(): Node {
return this.currentActivePanel;
}
/**
* 设置选中的角色ID
* @param girlId 角色ID
*/
public setSelectedGirlId(girlId: number): void {
this.selectedGirlId = girlId;
sys.localStorage.setItem(NavigationManager.LastSelectGirlID, girlId);
logger.log("[NavigationManager] Selected girl ID set to:", girlId);
}
/**
* 获取当前选中的角色ID
* @returns 当前选中的角色ID
*/
public getSelectedGirlId(): number {
return this.selectedGirlId;
}
/**
* 设置选中的主题ID
* @param themeId 主题ID
*/
public setSelectedCategoryId(themeId: number): void {
this.selectedCategoryId = themeId;
logger.log("[NavigationManager] Selected theme ID set to:", themeId);
}
/**
* 获取当前选中的主题ID
* @returns 当前选中的主题ID
*/
public getSelectedCategoryId(): number {
return this.selectedCategoryId;
}
}