Skip to content

Commit 9d935c4

Browse files
wehosHongzhi Wenclaude
authored
fix: 模型管理页未保存提示全面化 — 快照比对替代零散 dirty 标记 (Project-N-E-K-O#620)
1. 初始化加载角色模型时 dispatchEvent('change') 误触发 hasUnsavedChanges, 导致什么都没改也弹"未保存"提示。 2. 打光、待机动作等设置修改后退出不会触发未保存提示。 改为快照比对机制:初始化完成后记录所有可保存设置的快照,退出前对比当前值 和快照,不一致才弹提示。同时在各打光/待机动作 handler 中追加 dirty 标记, 让 beforeunload 等检查点也能正确拦截。 Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 39e2462 commit 9d935c4

1 file changed

Lines changed: 58 additions & 27 deletions

File tree

static/js/model_manager.js

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,41 @@ function sendMessageToMainPage(action, payload = {}) {
504504
// 全局变量:跟踪未保存的更改
505505
window.hasUnsavedChanges = false;
506506

507-
// 仅当本页确实保存过配置时,才触发主界面重载(避免退出就把主界面模型/位置“复位”)
507+
// 采集当前所有可保存设置的快照(模型选择 + 打光 + 待机动作)
508+
function captureSettingsSnapshot() {
509+
const modelSelect = document.getElementById('model-select');
510+
const vrmModelSelect = document.getElementById('vrm-model-select');
511+
return {
512+
modelType: typeof currentModelType !== 'undefined' ? currentModelType : '',
513+
live2d: modelSelect ? modelSelect.value : '',
514+
live3d: vrmModelSelect ? vrmModelSelect.value : '',
515+
// VRM 打光
516+
ambient: document.getElementById('ambient-light-slider')?.value ?? '',
517+
mainLight: document.getElementById('main-light-slider')?.value ?? '',
518+
exposure: document.getElementById('exposure-slider')?.value ?? '',
519+
toneMapping: document.getElementById('tonemapping-select')?.value ?? '',
520+
outlineWidth: document.getElementById('vrm-outline-width-slider')?.value ?? '',
521+
// MMD 打光
522+
mmdAmbientIntensity: document.getElementById('mmd-ambient-intensity-slider')?.value ?? '',
523+
mmdAmbientColor: document.getElementById('mmd-ambient-color-picker')?.value ?? '',
524+
mmdDirectionalIntensity: document.getElementById('mmd-directional-intensity-slider')?.value ?? '',
525+
mmdDirectionalColor: document.getElementById('mmd-directional-color-picker')?.value ?? '',
526+
mmdExposure: document.getElementById('mmd-exposure-slider')?.value ?? '',
527+
mmdToneMapping: document.getElementById('mmd-tonemapping-select')?.value ?? '',
528+
mmdOutline: String(document.getElementById('mmd-outline-toggle')?.checked ?? false),
529+
// 待机动作
530+
idleAnimation: document.getElementById('idle-animation-select')?.value ?? '',
531+
mmdIdleAnimation: document.getElementById('mmd-idle-animation-select')?.value ?? '',
532+
};
533+
}
534+
535+
// 比较两个快照是否一致
536+
function snapshotsEqual(a, b) {
537+
if (!a || !b) return false;
538+
return Object.keys(a).every(k => String(a[k]) === String(b[k]));
539+
}
540+
541+
// 仅当本页确实保存过配置时,才触发主界面重载(避免退出就把主界面模型/位置”复位”)
508542
window._modelManagerHasSaved = false;
509543
window._modelManagerLanlanName = new URLSearchParams(window.location.search).get('lanlan_name') || '';
510544
/**
@@ -1169,8 +1203,8 @@ document.addEventListener('DOMContentLoaded', async () => {
11691203
textSpanId: 'mmd-animation-select-text',
11701204
iconClass: 'mmd-animation-select-icon',
11711205
iconSrc: '/static/icons/motion_select_icon.png?v=1',
1172-
defaultText: '选择VMD动画',
1173-
iconAlt: '选择VMD动画',
1206+
defaultText: t('live2d.mmdAnimation.selectAnimation', '选择VMD动画'),
1207+
iconAlt: t('live2d.mmdAnimation.selectAnimation', '选择VMD动画'),
11741208
shouldSkipOption: (option) => {
11751209
return option.value === '' && (
11761210
option.textContent.includes('请先加载') ||
@@ -3214,7 +3248,7 @@ document.addEventListener('DOMContentLoaded', async () => {
32143248
mmdAnimations = (data.success && Array.isArray(data.animations)) ? data.animations : [];
32153249
if (!mmdAnimationSelect) return;
32163250

3217-
mmdAnimationSelect.innerHTML = '<option value="">选择VMD动画</option>';
3251+
mmdAnimationSelect.innerHTML = `<option value="">${t('live2d.mmdAnimation.selectAnimation', '选择VMD动画')}</option>`;
32183252
if (mmdAnimations.length > 0) {
32193253
mmdAnimations.forEach(anim => {
32203254
const animPath = anim.path || anim.url || (typeof anim === 'string' ? anim : null);
@@ -4080,6 +4114,7 @@ document.addEventListener('DOMContentLoaded', async () => {
40804114
if (vrmManager && vrmManager.ambientLight) {
40814115
vrmManager.ambientLight.intensity = value;
40824116
}
4117+
window.hasUnsavedChanges = true;
40834118
});
40844119
}
40854120

@@ -4091,6 +4126,7 @@ document.addEventListener('DOMContentLoaded', async () => {
40914126
if (vrmManager && vrmManager.mainLight) {
40924127
vrmManager.mainLight.intensity = value;
40934128
}
4129+
window.hasUnsavedChanges = true;
40944130
});
40954131
}
40964132

@@ -4146,6 +4182,7 @@ document.addEventListener('DOMContentLoaded', async () => {
41464182
if (vrmManager && vrmManager.renderer) {
41474183
vrmManager.renderer.toneMappingExposure = value;
41484184
}
4185+
window.hasUnsavedChanges = true;
41494186
});
41504187
}
41514188

@@ -4173,6 +4210,7 @@ document.addEventListener('DOMContentLoaded', async () => {
41734210
if (exposureValue) {
41744211
exposureValue.style.opacity = isNoToneMapping ? '0.5' : '1';
41754212
}
4213+
window.hasUnsavedChanges = true;
41764214
});
41774215
}
41784216

@@ -4201,6 +4239,7 @@ document.addEventListener('DOMContentLoaded', async () => {
42014239
if (vrmOutlineWidthSlider) {
42024240
vrmOutlineWidthSlider.addEventListener('input', (e) => {
42034241
applyVrmOutlineWidth(parseFloat(e.target.value));
4242+
window.hasUnsavedChanges = true;
42044243
});
42054244
}
42064245

@@ -4209,6 +4248,7 @@ document.addEventListener('DOMContentLoaded', async () => {
42094248
idleAnimationSelect.addEventListener('change', async (e) => {
42104249
const selectedUrl = e.target.value;
42114250
if (!selectedUrl) return;
4251+
window.hasUnsavedChanges = true;
42124252
// 实时切换待机动作:停止当前动画,播放新的循环动画
42134253
if (vrmManager && vrmManager.animation && vrmManager.currentModel) {
42144254
try {
@@ -4304,6 +4344,7 @@ document.addEventListener('DOMContentLoaded', async () => {
43044344
if (mmdIdleAnimationSelect) {
43054345
mmdIdleAnimationSelect.addEventListener('change', async (e) => {
43064346
const selectedUrl = e.target.value;
4347+
window.hasUnsavedChanges = true;
43074348
// 实时切换MMD待机动作:停止当前动画,播放新的循环动画
43084349
if (window.mmdManager && window.mmdManager.currentModel) {
43094350
try {
@@ -4462,6 +4503,7 @@ document.addEventListener('DOMContentLoaded', async () => {
44624503
const valEl = document.getElementById(valId);
44634504
if (valEl) valEl.textContent = fmt ? fmt(v) : v;
44644505
applyMmdSettings();
4506+
window.hasUnsavedChanges = true;
44654507
});
44664508
}
44674509
});
@@ -4476,6 +4518,7 @@ document.addEventListener('DOMContentLoaded', async () => {
44764518
const valEl = document.getElementById(valId);
44774519
if (valEl) valEl.textContent = e.target.value;
44784520
applyMmdSettings();
4521+
window.hasUnsavedChanges = true;
44794522
});
44804523
}
44814524
});
@@ -4495,6 +4538,7 @@ document.addEventListener('DOMContentLoaded', async () => {
44954538
if (mmdExposureValue) {
44964539
mmdExposureValue.style.opacity = isNoToneMapping ? '0.5' : '1';
44974540
}
4541+
window.hasUnsavedChanges = true;
44984542
});
44994543
}
45004544

@@ -4504,6 +4548,7 @@ document.addEventListener('DOMContentLoaded', async () => {
45044548
const statusEl = document.getElementById('mmd-outline-status');
45054549
if (statusEl) statusEl.textContent = e.target.checked ? 'ON' : 'OFF';
45064550
applyMmdSettings();
4551+
window.hasUnsavedChanges = true;
45074552
});
45084553
}
45094554

@@ -5379,11 +5424,7 @@ document.addEventListener('DOMContentLoaded', async () => {
53795424
if (modelSuccess) {
53805425
showStatus(t('live2d.settingsSaved', '模型设置保存成功!'), 2000);
53815426
window.hasUnsavedChanges = false;
5382-
window._savedModelSnapshot = {
5383-
modelType: currentModelType,
5384-
live2d: modelSelect ? modelSelect.value : '',
5385-
live3d: vrmModelSelect ? vrmModelSelect.value : '',
5386-
};
5427+
window._savedModelSnapshot = captureSettingsSnapshot();
53875428
window._modelManagerHasSaved = true;
53885429
} else {
53895430
showStatus(t('live2d.saveFailedGeneral', '保存失败!'), 2000);
@@ -5393,11 +5434,7 @@ document.addEventListener('DOMContentLoaded', async () => {
53935434
if (positionSuccess && modelSuccess) {
53945435
showStatus(t('live2d.settingsSaved', '位置和模型设置保存成功!'), 2000);
53955436
window.hasUnsavedChanges = false; // 保存成功后重置标志
5396-
window._savedModelSnapshot = {
5397-
modelType: currentModelType,
5398-
live2d: modelSelect ? modelSelect.value : '',
5399-
live3d: vrmModelSelect ? vrmModelSelect.value : '',
5400-
};
5437+
window._savedModelSnapshot = captureSettingsSnapshot();
54015438
window._modelManagerHasSaved = true;
54025439
// 不在保存时立即通知主页,而是在返回主页时通知
54035440
// sendMessageToMainPage('reload_model');
@@ -5407,6 +5444,7 @@ document.addEventListener('DOMContentLoaded', async () => {
54075444
window._modelManagerHasSaved = true;
54085445
} else if (modelSuccess) {
54095446
showStatus(t('live2d.modelSavedPositionFailed', '模型设置保存成功,位置保存失败!'), 2000);
5447+
window._savedModelSnapshot = captureSettingsSnapshot();
54105448
window._modelManagerHasSaved = true;
54115449
// 不在保存时立即通知主页,而是在返回主页时通知
54125450
// sendMessageToMainPage('reload_model');
@@ -5467,14 +5505,9 @@ document.addEventListener('DOMContentLoaded', async () => {
54675505
_earlyBackBtn.removeEventListener('click', _earlyBackHandler);
54685506
}
54695507
backToMainBtn.addEventListener('click', async () => {
5470-
// 检查是否有未保存的更改:先比对当前模型选择和已保存快照,一致则视为无更改
5508+
// 退出前:比对当前设置和已保存快照,完全一致则视为无更改
54715509
if (window.hasUnsavedChanges && window._savedModelSnapshot) {
5472-
const snap = window._savedModelSnapshot;
5473-
// 只比较当前激活分支的模型选择,隐藏分支的变化不触发未保存提示
5474-
const branchMatch = currentModelType === 'live2d'
5475-
? (modelSelect ? modelSelect.value : '') === snap.live2d
5476-
: (vrmModelSelect ? vrmModelSelect.value : '') === snap.live3d;
5477-
if (currentModelType === snap.modelType && branchMatch) {
5510+
if (snapshotsEqual(window._savedModelSnapshot, captureSettingsSnapshot())) {
54785511
window.hasUnsavedChanges = false;
54795512
}
54805513
}
@@ -7144,12 +7177,10 @@ document.addEventListener('DOMContentLoaded', async () => {
71447177
}
71457178
}
71467179

7147-
// 记录初始化完成时各 select 的已保存值,用于退出前判断模型是否真的被改过
7148-
window._savedModelSnapshot = {
7149-
modelType: currentModelType,
7150-
live2d: modelSelect ? modelSelect.value : '',
7151-
live3d: vrmModelSelect ? vrmModelSelect.value : '',
7152-
};
7180+
// 等待异步设置(打光、待机动作等)加载完成后记录快照
7181+
setTimeout(() => {
7182+
window._savedModelSnapshot = captureSettingsSnapshot();
7183+
}, 500);
71537184
} catch (_fatalError) {
71547185
console.error('[模型管理] DOMContentLoaded 致命错误:', _fatalError);
71557186
const _s = document.getElementById('status-text');

0 commit comments

Comments
 (0)