Appearance
附录:隐形的面板 — 代码块全集
所有代码块均从正文中移出。编号对应正文中的引用标记。
1. reset() 方法 — 初始版本
typescript
reset(level: PopupLevel): void {
this.levels = [level];
this.activeIdx = 0;
this.buildPanel(this.panels[0], level);
this.panels[1].style.display = "none";
this.inner.style.transform = "translateX(0)";
this.inner.style.transition = "none";
this.onAfterRender?.(level, this);
}2. reset() 断点逐行执行
this.levels = [level]; → display 还是 ""
this.activeIdx = 0; → display 还是 ""
this.buildPanel(this.panels[0], level); → 构建内容,display 还是 ""
this.panels[1].style.display = "none"; → panels[1] 变 none,panels[0] 还是 ""
this.inner.style.transform = "translateX(0)"; → 还是 ""
this.inner.style.transition = "none"; → 还是 ""3. _animateForward 交换面板引用
typescript
// 动画结束后,交换面板角色
const tmp = this.panels[0];
this.panels[0] = this.panels[1];
this.panels[1] = tmp;4. 新 panels[1] 设为 none
typescript
this.panels[1].style.display = "none";5. reset() 方法 — 重复出现(带注释版)
typescript
reset(level: PopupLevel): void {
this.levels = [level];
this.activeIdx = 0;
this.buildPanel(this.panels[0], level); // panels[0] 是元素 B,可见 ✅
this.panels[1].style.display = "none"; // panels[1] 是元素 A,本来就是 none
// ...
}6. _animateBack 开头:恢复 panels[1] 可见
typescript
private _animateBack(newLevel: PopupLevel): void {
this.transitioning = true;
this.panels[1].style.display = ""; // ← 这里!
this.buildPanel(this.panels[1], newLevel);
// ...
}7. 模型弹窗 DOM 结构(白屏时)
html
div#modelPopup.overlay.slide-menu.visible
div.slide-inner
div.slide-panel ← panels[0], display: none
div.slide-panel ← panels[1], display: none8. reset() 方法 — 第三次出现(带 transitioning 重置)
typescript
reset(level: PopupLevel): void {
this.levels = [level];
this.activeIdx = 0;
this.transitioning = false;
this.buildPanel(this.panels[0], level);
this.panels[1].style.display = "none";
this.inner.style.transform = "translateX(0)";
this.inner.style.transition = "none";
this.onAfterRender?.(level, this);
}9. 日志:第一次打开模型弹窗
constructor: panels[0].display="", panels[1].display=""
reset: before: panels[0].display="", panels[1].display=""
reset: after: panels[0].display="", panels[1].display="none"10. 日志:进入文件夹
_animateForward: before: panels[0].display="", panels[1].display="none"
_animateForward: 构建 panels[1] 内容
_animateForward: 动画开始
_animateForward: 动画结束,交换引用
_animateForward: after: panels[0].display="", panels[1].display="none"11. 日志:返回
_animateBack: before: panels[0].display="", panels[1].display="none"
_animateBack: panels[1].display = ""
_animateBack: after set display: panels[0].display="", panels[1].display=""
_animateBack: 构建 panels[1] 内容
_animateBack: 动画开始
_animateBack: 动画结束,交换引用
_animateBack: after: panels[0].display="", panels[1].display="none"12. 日志:关闭模型弹窗
(closeAllOverlays 只移除 visible 类,不碰 SlideMenu 内部状态)13. 日志:再次打开模型弹窗(白屏!)
reset: before: panels[0].display="none", panels[1].display="none"14. 构造函数:panels 初始化为空数组
typescript
constructor(container: HTMLElement, options?) {
this.panels = [];
// 创建两个 panel,push 进 panels
}15. 构造函数:从容器中 querySelector 找面板
typescript
constructor(container: HTMLElement, options?) {
this.container = container;
this.inner = container.querySelector(".slide-inner")!;
this.panels = Array.from(this.inner.querySelectorAll(".slide-panel")) as HTMLElement[];
// ...
}16. 查询:环境弹窗的 inner 里只有 2 个 panel
javascript
document.querySelectorAll("#sceneOverlay .slide-inner .slide-panel").length返回值:2
17. 四个 SlideMenu 实例创建
typescript
export const modelSlideMenu = new SlideMenu(dom.modelPopup!, { ... });
export const motionSlideMenu = new SlideMenu(dom.motionPopup!, { ... });
export const sceneSlideMenu = new SlideMenu(dom.sceneOverlay!, { ... });
export const settingsSlideMenu = new SlideMenu(dom.settingsOverlay!, { ... });18. #modelPopup 的 HTML 结构
html
<div id="modelPopup" class="overlay slide-menu">
<div class="slide-inner">
<div class="slide-panel"></div>
<div class="slide-panel"></div>
</div>
</div>19. #sceneOverlay 的 HTML 结构
html
<div id="sceneOverlay" class="overlay slide-menu">
<div class="slide-inner">
<div class="slide-panel"></div>
<div class="slide-panel"></div>
</div>
</div>20. 加 id 后测试:发现环境弹窗的 panels[1] 是模型的 panel
_animateForward after: panels[0].id = "env-panel-1", panels[1].id = "model-panel-0"21. 查询:环境弹窗 inner 里竟有 4 个 panel
javascript
document.querySelectorAll("#sceneOverlay .slide-inner .slide-panel").length返回值:4
22. 查询:模型弹窗 inner 里还是 2 个
javascript
document.querySelectorAll("#modelPopup .slide-inner .slide-panel").length返回值:2
23. 构造函数:不够 2 个 panel 时补建
typescript
constructor(container: HTMLElement, options?) {
this.container = container;
this.inner = container.querySelector(".slide-inner")!;
this.panels = Array.from(this.inner.querySelectorAll(".slide-panel")) as HTMLElement[];
while (this.panels.length < 2) {
const panel = document.createElement("div");
panel.className = "slide-panel";
this.inner.appendChild(panel);
this.panels.push(panel);
}
// ...
}24. 构造函数日志
typescript
console.log("constructor:", container.id, "panels found:", this.panels.length);25. 初始化控制台输出
constructor: modelPopup panels found: 2
constructor: motionPopup panels found: 2
constructor: sceneOverlay panels found: 2
constructor: settingsOverlay panels found: 226. 调用栈:每次 showEnvMenu 都 new SlideMenu
new SlideMenu
showEnvMenu
showSceneMenu
onclick27. showEnvMenu 每次 new SlideMenu
typescript
export async function showEnvMenu() {
const m = await import("./env-menu");
// ...
const slideMenu = new SlideMenu(dom.sceneOverlay!, { ... });
slideMenu.reset(buildEnvLevel());
}28. showModelPopup 用单例
typescript
export function showModelPopup() {
// ...
modelSlideMenu.reset(buildLibraryLevel());
}29. 构造函数:找到的 panel 不足时补建
typescript
this.panels = Array.from(this.inner.querySelectorAll(".slide-panel")) as HTMLElement[];
while (this.panels.length < 2) {
// 创建新 panel
}30. 构造函数日志:第一次打开环境弹窗
constructor: found 2 panels31. 构造函数日志:第二次打开环境弹窗
constructor: found 4 panels32. 最终修复:reset() 加一行
typescript
reset(level: PopupLevel): void {
this.levels = [level];
this.activeIdx = 0;
this.transitioning = false;
this.panels[0].style.display = ""; // ← 新增
this.buildPanel(this.panels[0], level);
this.panels[1].style.display = "none";
this.inner.style.transform = "translateX(0)";
this.inner.style.transition = "none";
this.onAfterRender?.(level, this);
}