fix(monitor): Raw Socket 記住使用者與圖片偏好
Docs/ChangeHistory.mdTest/monitor_audience_ui.cjsassets/app.css.build.jsonassets/monitor.js
commit e4d6054f8818d02319768d810a173e7af63a8238Author: Nuclear0709 <agp05432@gmail.com>Date: Wed Nov 11 01:13:16 2026 +0800 fix(monitor): Raw Socket 記住使用者與圖片偏好 - raw prefs 保存 user/image/gift,重新整理後恢復並預覽 - MediaCache/emoji 選擇立即寫入偏好 - 清除測試設定同步清除 raw prefs - UI 測試補圖片持久化 diff --git a/Docs/ChangeHistory.md b/Docs/ChangeHistory.md--- a/Docs/ChangeHistory.md+++ b/Docs/ChangeHistory.md@@ -11,7 +11,10 @@ - 選 emoji 使用目前 `/api/emoji/preview` 的 URL。 - AdOverlay 模式下同一選擇會同步填入 iconURL。 - Raw 圖片欄新增 HTTP(S)/data URI 預覽,選單選擇後立即更新。-- UI 測試補 Raw 圖片選單與預覽。+- Raw Socket 的使用者名稱與圖片/禮物欄位會存到 `ttw-monitor-raw-prefs`,重新整理後自動帶回;流程測試沿用原本的 `ttw-monitor-test-prefs`。+- MediaCache/emoji 選單選到的頭貼也會一併記憶。+- 「清除測試設定」會同時清除兩份使用者偏好。+- UI 測試補 Raw 圖片選單、預覽與使用者名稱持久化。 ### 驗證 diff --git a/Test/monitor_audience_ui.cjs b/Test/monitor_audience_ui.cjs--- a/Test/monitor_audience_ui.cjs+++ b/Test/monitor_audience_ui.cjs@@ -180,6 +180,9 @@ async function main() { const image = document.querySelector('#monRawImagePreview'); return image && !image.hidden && image.src.endsWith('/emoji.png'); });+ await page.evaluate(() => { document.querySelector('#monRawPanel').open = true; });+ await page.type('#monRawUser', 'Raw User');+ assert.equal(await page.$eval('#monRawUser', node => node.value), 'Raw User'); await page.waitForFunction(() => { const image = document.querySelector('#monTestAvatarPreview'); return image && !image.hidden && image.src.endsWith('/emoji.png');@@ -244,6 +247,8 @@ async function main() { assert.notEqual(posts[1].data.testId, posts[0].data.testId); assert.match(await page.$eval('#monTestStatus', node => node.textContent), /去重保護/); + const rawPrefsBeforeReload = await page.evaluate(() => localStorage.getItem('ttw-monitor-raw-prefs'));+ assert.match(rawPrefsBeforeReload || '', /Raw User/); await page.reload({ waitUntil: 'load' }); await page.waitForFunction(() => document.querySelectorAll('#monTestCategory option').length >= 2); await page.evaluate(() => { document.querySelector('#monTestPanel').open = true; });@@ -251,6 +256,12 @@ async function main() { const image = document.querySelector('#monTestAvatarPreview'); return image && !image.hidden && image.src.endsWith('/avatar.png'); });+ assert.equal(await page.$eval('#monRawUser', node => node.value), 'Raw User');+ assert.equal(await page.$eval('#monRawImage', node => node.value), rawEmoji);+ await page.waitForFunction(() => {+ const image = document.querySelector('#monRawImagePreview');+ return image && !image.hidden && image.src.endsWith('/emoji.png');+ }); const restored = await page.evaluate(() => ({ user: document.querySelector('#monTestUser').value, avatar: document.querySelector('#monTestAvatar').value,diff --git a/assets/app.css.build.json b/assets/app.css.build.json--- a/assets/app.css.build.json+++ b/assets/app.css.build.json@@ -1,5 +1,5 @@ { "schema": 2,- "inputHash": "2fb160324cbdeec2df88a8bb855e3bb50e01fb4d01f6ef3b9362c1269597add5",+ "inputHash": "d5695138279146aa1fca53e5fda83ba8441f95737fedabfa82f6ffba142846f4", "outputHash": "5e8e8ac9b7a490acf78fc06ee9e9c0e63e69a0a32e695f93025dce6a24930dce" }diff --git a/assets/monitor.js b/assets/monitor.js--- a/assets/monitor.js+++ b/assets/monitor.js@@ -93,6 +93,7 @@ const rawSend = document.getElementById('monRawSend'); const rawStatus = document.getElementById('monRawStatus'); const TEST_PREFS_KEY = 'ttw-monitor-test-prefs';+ const RAW_PREFS_KEY = 'ttw-monitor-raw-prefs'; let emojiMap = null; let emojiPattern = null;@@ -388,6 +389,31 @@ } catch { /* 無痕模式或儲存額度不足時不影響測試 */ } } + function loadRawPrefs() {+ try {+ const data = JSON.parse(localStorage.getItem(RAW_PREFS_KEY) || 'null');+ return data && typeof data === 'object' ? data : null;+ } catch { return null; }+ }++ function saveRawPrefs() {+ try {+ localStorage.setItem(RAW_PREFS_KEY, JSON.stringify({+ user: rawUser.value,+ image: rawImage.value,+ gift: rawGift.value+ }));+ } catch { /* ignore */ }+ }++ function restoreRawPrefs() {+ const saved = loadRawPrefs();+ if (!saved) return;+ if (typeof saved.user === 'string') rawUser.value = saved.user;+ if (typeof saved.image === 'string') rawImage.value = saved.image;+ if (typeof saved.gift === 'string') rawGift.value = saved.gift;+ }+ function restoreTestPrefs() { if (!savedTestPrefs) return; if (typeof savedTestPrefs.user === 'string') testUser.value = savedTestPrefs.user;@@ -478,7 +504,12 @@ clearTestPending(); clearTestCooldown(); try { localStorage.removeItem(TEST_PREFS_KEY); } catch { /* ignore */ }+ try { localStorage.removeItem(RAW_PREFS_KEY); } catch { /* ignore */ } savedTestPrefs = null;+ rawUser.value = '';+ rawImage.value = '';+ rawGift.value = '';+ rawImagePick.value = ''; testUser.value = ''; testAvatar.value = ''; testAvatarPick.value = '';@@ -489,6 +520,7 @@ testSocket.checked = true; testNotify.checked = false; updateAvatarPreview();+ updateRawImagePreview(); setTestStatus(t('monitor.test.cleared', {}, '已清除測試設定。')); } @@ -1192,15 +1224,21 @@ if (!rawImagePick.value) return; rawImage.value = rawImagePick.value; if (rawType.value === 'AdOverlay') rawGift.value = rawImagePick.value;+ saveRawPrefs();+ updateRawImagePreview();+ });+ rawImage.addEventListener('input', () => {+ saveRawPrefs(); updateRawImagePreview(); });- rawImage.addEventListener('input', updateRawImagePreview);+ rawGift.addEventListener('input', saveRawPrefs); rawSend.addEventListener('click', sendRawSocketTest); testAdd.addEventListener('click', addTestMessage); testSend.addEventListener('click', sendSocketTest); testClear.addEventListener('click', clearTestPrefs); testUser.addEventListener('input', saveTestPrefs);+ rawUser.addEventListener('input', saveRawPrefs); testAvatar.addEventListener('input', () => { saveTestPrefs(); updateAvatarPreview(); }); testPlatform.addEventListener('change', saveTestPrefs); testCategory.addEventListener('change', saveTestPrefs);@@ -1237,6 +1275,7 @@ applyTestI18n(); buildTestCategories(); restoreTestPrefs();+ restoreRawPrefs(); restoreTestCategory(); updateAvatarPreview(); updateCount();