StockRecommander / static /theme-manager.js
zhengyi
fix: 修復深色模式按鈕事件綁定
c06c940
/**
* 主題管理 JavaScript 模組
* 處理 localStorage 儲存、讀取和主題切換
*/
class ThemeManager {
constructor() {
this.STORAGE_KEY = 'theme-preference';
this.LIGHT_THEME = 'light';
this.DARK_THEME = 'dark';
this.SYSTEM_PREFERENCE = 'system';
// 初始化主題
this.initTheme();
}
/**
* 初始化主題 - 在應用程式啟動時調用
*/
initTheme() {
const savedTheme = this.getSavedTheme();
const systemPreference = this.getSystemPreference();
// 決定要使用的主題
let themeToApply = this.LIGHT_THEME; // 預設亮色
if (savedTheme) {
// 使用儲存的用戶偏好
themeToApply = savedTheme;
} else if (systemPreference === this.DARK_THEME) {
// 使用系統偏好(如果沒有儲存的選擇)
themeToApply = this.DARK_THEME;
}
this.applyTheme(themeToApply);
this.updateThemeToggleButton(themeToApply);
}
/**
* 從 localStorage 讀取儲存的主題偏好
* @returns {string|null} 儲存的主題名稱或 null
*/
getSavedTheme() {
try {
if (typeof(Storage) !== "undefined" && localStorage) {
const saved = localStorage.getItem(this.STORAGE_KEY);
if (saved === this.LIGHT_THEME || saved === this.DARK_THEME) {
return saved;
}
}
} catch (e) {
console.warn('無法存取 localStorage:', e);
}
return null;
}
/**
* 儲存主題偏好至 localStorage
* @param {string} theme - 要儲存的主題名稱
*/
saveTheme(theme) {
try {
if (typeof(Storage) !== "undefined" && localStorage) {
localStorage.setItem(this.STORAGE_KEY, theme);
}
} catch (e) {
console.warn('無法儲存主題至 localStorage:', e);
}
}
/**
* 取得系統偏好設定(使用 CSS Media Query)
* @returns {string} 'dark' 或 'light'
*/
getSystemPreference() {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return this.DARK_THEME;
}
return this.LIGHT_THEME;
}
/**
* 套用主題至 DOM
* @param {string} theme - 要套用的主題名稱
*/
applyTheme(theme) {
const validTheme = (theme === this.DARK_THEME) ? this.DARK_THEME : this.LIGHT_THEME;
// 設定 data-theme 屬性至根元素
if (document.documentElement) {
document.documentElement.setAttribute('data-theme', validTheme);
}
// 設定 body 屬性(備選方案)
if (document.body) {
document.body.setAttribute('data-theme', validTheme);
}
// 保存用戶選擇
this.saveTheme(validTheme);
// 觸發主題改變事件
this.dispatchThemeChangeEvent(validTheme);
}
/**
* 獲取當前套用的主題
* @returns {string} 當前主題名稱
*/
getCurrentTheme() {
const htmlTheme = document.documentElement?.getAttribute('data-theme');
if (htmlTheme === this.DARK_THEME) {
return this.DARK_THEME;
}
return this.LIGHT_THEME;
}
/**
* 切換主題
*/
toggleTheme() {
const currentTheme = this.getCurrentTheme();
const newTheme = currentTheme === this.DARK_THEME ? this.LIGHT_THEME : this.DARK_THEME;
this.applyTheme(newTheme);
this.updateThemeToggleButton(newTheme);
// 觸發 Plotly 圖表更新事件(如果存在)
this.notifyChartThemeChange(newTheme);
}
/**
* 更新主題切換按鈕的文字和狀態
* @param {string} theme - 當前主題
*/
updateThemeToggleButton(theme) {
const buttons = document.querySelectorAll('.theme-toggle-btn, [data-theme-toggle]');
buttons.forEach(btn => {
if (theme === this.DARK_THEME) {
btn.textContent = '☀️ 亮色模式';
btn.setAttribute('aria-label', '切換至亮色模式');
} else {
btn.textContent = '🌙 深色模式';
btn.setAttribute('aria-label', '切換至深色模式');
}
});
}
/**
* 觸發主題改變事件
* @param {string} theme - 新主題名稱
*/
dispatchThemeChangeEvent(theme) {
const event = new CustomEvent('themechange', {
detail: { theme: theme }
});
window.dispatchEvent(event);
}
/**
* 通知圖表元件主題已改變
* @param {string} theme - 新主題名稱
*/
notifyChartThemeChange(theme) {
// 觸發自定義事件以通知 Plotly 圖表
const event = new CustomEvent('chartthemechange', {
detail: { theme: theme }
});
window.dispatchEvent(event);
}
/**
* 監聽系統主題偏好的改變
*/
watchSystemPreference() {
if (window.matchMedia) {
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
// 使用 addEventListener(新 API)
if (darkModeQuery.addEventListener) {
darkModeQuery.addEventListener('change', (e) => {
// 僅在沒有用戶選擇的情況下應用系統偏好
if (!this.getSavedTheme()) {
const newTheme = e.matches ? this.DARK_THEME : this.LIGHT_THEME;
this.applyTheme(newTheme);
}
});
}
}
}
/**
* 設置主題切換按鈕的點擊事件
*/
setupThemeButtons() {
// 尋找所有帶有 theme-toggle-btn 類別的元素
const findAndSetupButtons = () => {
const buttons = document.querySelectorAll('.theme-toggle-btn');
if (buttons.length === 0) {
// 如果未找到,稍後重試(Gradio 可能還在載入)
setTimeout(findAndSetupButtons, 100);
return;
}
buttons.forEach((btn) => {
// 檢查是否已經綁定(防止重複)
if (btn._themeToggleSetup) {
return;
}
btn._themeToggleSetup = true;
// 綁定點擊事件
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.toggleTheme();
});
// 鍵盤導航支援 (Enter 和 Space)
btn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
this.toggleTheme();
}
});
console.log('✅ 深色模式按鈕已設置:', btn);
});
};
findAndSetupButtons();
}
}
// 在頁面加載時初始化主題管理器
function initializeThemeManager() {
console.log('🌙 初始化主題管理器...');
window.themeManager = new ThemeManager();
window.themeManager.watchSystemPreference();
// 立即設置按鈕
window.themeManager.setupThemeButtons();
// 定期檢查按鈕(Gradio 可能動態添加元素)
setInterval(() => {
const buttons = document.querySelectorAll('.theme-toggle-btn');
buttons.forEach((btn) => {
if (!btn._themeToggleSetup) {
window.themeManager.setupThemeButtons();
}
});
}, 500);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeThemeManager);
} else {
// 如果 DOM 已加載,直接初始化
initializeThemeManager();
}
// 監聽 Gradio 的自定義加載事件(如果存在)
if (window.gradio) {
// Gradio 可能有自己的事件系統
console.log('🎵 Gradio 已檢測到');
}
// 導出以便在其他模組中使用
if (typeof module !== 'undefined' && module.exports) {
module.exports = ThemeManager;
}