@@ -3254,6 +3254,23 @@ class UniversalTutorialManager {
32543254 delete element . dataset . tutorialDisabled ;
32553255 }
32563256 } ) ;
3257+ // 兜底:扫描整个文档中残留的 data-tutorial-disabled 节点。
3258+ // 模型管理 MMD 教程结束后曾出现 #vrm-model-select-btn / #mmd-animation-select-btn
3259+ // 等按钮被 pointer-events:none 卡死的情况,根因是 await 期间集合被提前 clear,
3260+ // 后续被遗漏。这里独立做一遍 DOM 兜底清理。
3261+ // 优先从 tutorialInteractionStates 还原原始 inline 值,仅在没有保存态时
3262+ // 退化为清空,避免误把页面上原本就 pointer-events:none 的元素重新激活。
3263+ try {
3264+ document . querySelectorAll ( '[data-tutorial-disabled]' ) . forEach ( element => {
3265+ const state = this . tutorialInteractionStates . get ( element ) ;
3266+ element . style . pointerEvents = state ?. pointerEvents || '' ;
3267+ element . style . cursor = state ?. cursor || '' ;
3268+ element . style . userSelect = state ?. userSelect || '' ;
3269+ delete element . dataset . tutorialDisabled ;
3270+ } ) ;
3271+ } catch ( error ) {
3272+ console . warn ( '[Tutorial] 扫描残留 tutorial-disabled 元素失败:' , error ) ;
3273+ }
32573274 this . tutorialInteractionStates . clear ( ) ;
32583275 this . tutorialControlledElements = new Set ( ) ;
32593276 this . tutorialMarkerDisplayCache = null ;
@@ -3743,7 +3760,25 @@ class UniversalTutorialManager {
37433760 } , 500 ) ;
37443761
37453762 // 监听事件
3746- this . driver . on ( 'destroy' , ( ) => this . onTutorialEnd ( ) ) ;
3763+ // driver.on('destroy') 触发后,先做关键 UI 清理(移除跳过按钮 +
3764+ // 还原 pointer-events),再走完整的 onTutorialEnd 流程。
3765+ // 这两步独立 try/catch,确保即便 onTutorialEnd 任一环节抛错或被早返回,
3766+ // 也不会留下右上角残余按钮 / 模型列表锁死。
3767+ // (MMD/VRM 模型管理教程结束后跳过按钮残留 & 模型列表锁死的修复路径。)
3768+ this . driver . on ( 'destroy' , ( ) => {
3769+ console . log ( '[Tutorial] driver destroy → 执行关键 UI 清理' ) ;
3770+ try {
3771+ this . hideSkipButton ( ) ;
3772+ } catch ( error ) {
3773+ console . warn ( '[Tutorial] destroy 清理 hideSkipButton 失败:' , error ) ;
3774+ }
3775+ try {
3776+ this . restoreTutorialInteractionState ( ) ;
3777+ } catch ( error ) {
3778+ console . warn ( '[Tutorial] destroy 清理 restoreTutorialInteractionState 失败:' , error ) ;
3779+ }
3780+ this . onTutorialEnd ( ) ;
3781+ } ) ;
37473782 this . driver . on ( 'next' , ( ) => this . onStepChange ( ) . catch ( err => {
37483783 console . error ( '[Tutorial] 步骤切换失败:' , err ) ;
37493784 } ) ) ;
@@ -4677,6 +4712,23 @@ class UniversalTutorialManager {
46774712 * 因此既能给正常结束(onTutorialEnd)复用,也能给启动失败的回退路径复用。
46784713 */
46794714 _teardownTutorialUI ( ) {
4715+ // 关键 UI 清理:必须先于 _teardownPromise early-return 守卫执行,
4716+ // 且必须幂等。MMD 模型管理教程曾出现:用户走到末步点「完成」后
4717+ // 跳过按钮残留、模型列表按钮 pointer-events:none 卡死。
4718+ // 根因是 teardown 触发时 _teardownPromise 已被前一次未完成的链占用,
4719+ // early-return 直接跳过了 hideSkipButton / restoreTutorialInteractionState。
4720+ // 把这两个操作提到守卫之前,可在任何重复/并发调用下都保证用户能继续操作。
4721+ try {
4722+ this . hideSkipButton ( ) ;
4723+ } catch ( error ) {
4724+ console . warn ( '[Tutorial] hideSkipButton 失败:' , error ) ;
4725+ }
4726+ try {
4727+ this . restoreTutorialInteractionState ( ) ;
4728+ } catch ( error ) {
4729+ console . warn ( '[Tutorial] restoreTutorialInteractionState 失败:' , error ) ;
4730+ }
4731+
46804732 if ( this . _teardownPromise ) {
46814733 return this . _teardownPromise ;
46824734 }
@@ -4694,9 +4746,6 @@ class UniversalTutorialManager {
46944746 this . _tutorialEndRawReason = null ;
46954747 this . currentTutorialStartSource = 'auto' ;
46964748
4697- // 移除跳过按钮
4698- this . hideSkipButton ( ) ;
4699-
47004749 // 清除刷新定时器
47014750 if ( this . _refreshTimers ) {
47024751 this . _refreshTimers . forEach ( t => clearTimeout ( t ) ) ;
0 commit comments