const PageDashboard = {
_el: null,
_currentCategory: 'all',
_selectedGameId: null,
_marketMode: false,
_marketToken: null,
init() {
this._el = document.getElementById('page-dashboard');
this._marketToken = localStorage.getItem('turbosu-market-token');
},
async render() {
const cfg = await API.getConfig();
this._selectedGameId = cfg.selected_game_id;
this._el.innerHTML = this._template();
await this._loadCategories();
await this._loadThemes();
this._bindEvents();
},
async _loadCategories() {
try {
const cats = await API.getCategories();
const listEl = document.getElementById('dash-category-list');
if (!listEl) return;
const allItem = document.createElement('div');
allItem.className = 'category-item active';
allItem.textContent = '全部';
allItem.dataset.cat = 'all';
listEl.appendChild(allItem);
cats.forEach(cat => {
const item = document.createElement('div');
item.className = 'category-item';
item.textContent = cat;
item.dataset.cat = cat;
listEl.appendChild(item);
});
} catch (e) {
console.error('Failed to load categories:', e);
}
},
async _loadThemes() {
const gridEl = document.getElementById('dash-theme-grid');
if (!gridEl) return;
gridEl.innerHTML = '
加载中...
';
if (this._marketMode) {
await this._loadMarket(gridEl);
return;
}
try {
let themes = await API.getDashboards(this._currentCategory);
if (!themes || themes.length === 0) { gridEl.innerHTML = this._emptyHtml(); return; }
if (this._selectedGameId) { themes = themes.filter(t => this._isGameSupported(t)); }
const searchQuery = document.getElementById('dash-search')?.value?.toLowerCase() || '';
if (searchQuery) { themes = themes.filter(t => (t.name||'').toLowerCase().includes(searchQuery) || (t.description||'').toLowerCase().includes(searchQuery) || (t.author||'').toLowerCase().includes(searchQuery)); }
const sortBy = document.getElementById('dash-sort')?.value || 'name';
themes.sort((a, b) => (a[sortBy]||'').localeCompare(b[sortBy]||''));
if (themes.length === 0) { gridEl.innerHTML = this._emptyHtml('当前游戏没有兼容的仪表盘'); return; }
gridEl.innerHTML = themes.map(t => this._themeCardHtml(t)).join('');
} catch (e) { gridEl.innerHTML = '加载失败
'; }
},
async _loadMarket(gridEl) {
try {
const res = await fetch('/api/market/dashboards');
const data = await res.json();
const items = data.items || [];
if (!items.length) { gridEl.innerHTML = this._emptyHtml('市场暂无仪表盘'); return; }
gridEl.innerHTML = items.map(d => {
const icon = d.icon || '📦';
return `
${icon}
${d.name||'Unnamed'}
${d.category||''}v${d.version||'1.0'}
${d.author||''}${d.downloads||0} 次下载
`;
}).join('');
} catch (e) { gridEl.innerHTML = '市场加载失败
'; }
},
_isGameSupported(theme) {
const supported = theme.supported_games || 'all';
if (supported === 'all') return true;
const ids = supported.split(',').map(s => s.trim());
return ids.includes(this._selectedGameId);
},
_emptyHtml(msg) {
return `
${msg || '暂无仪表盘主题'}
创建或导入你的仪表盘主题
`;
},
_bindEvents() {
const categoryList = document.getElementById('dash-category-list');
if (categoryList) {
categoryList.addEventListener('click', (e) => {
const item = e.target.closest('.category-item');
if (!item) return;
categoryList.querySelectorAll('.category-item').forEach(el => el.classList.remove('active'));
item.classList.add('active');
this._currentCategory = item.dataset.cat;
this._loadThemes();
});
}
const gridEl = document.getElementById('dash-theme-grid');
if (gridEl) {
gridEl.addEventListener('click', (e) => {
const card = e.target.closest('.theme-card');
if (!card) return;
const themeId = card.dataset.themeId;
if (themeId) this._openDashboard(themeId);
const installBtn = e.target.closest('.btn-market-install');
if (installBtn) this._installFromMarket(installBtn.dataset.id, installBtn.dataset.file);
});
gridEl.addEventListener('contextmenu', (e) => {
const card = e.target.closest('.theme-card');
if (!card || !card.dataset.themeId) return;
e.preventDefault();
this._exportDashboard(card.dataset.themeId);
});
}
document.getElementById('dash-search')?.addEventListener('input', () => this._loadThemes());
document.getElementById('dash-sort')?.addEventListener('change', () => this._loadThemes());
document.getElementById('btn-new-dashboard')?.addEventListener('click', () => this._createNew());
document.getElementById('btn-import-dashboard')?.addEventListener('click', () => {
const input = document.createElement('input'); input.type = 'file'; input.accept = '.tsd';
input.onchange = async (e) => {
const file = e.target.files[0]; if (!file) return;
const fd = new FormData(); fd.append('file', file);
const res = await fetch('/api/dashboards/import', { method: 'POST', body: fd });
const data = await res.json();
if (data.ok) { Toast.show('导入成功', 'success'); this._loadThemes(); }
else Toast.show('导入失败', 'error');
}; input.click();
});
document.getElementById('btn-market-upload')?.addEventListener('click', () => this._showMarketUpload());
document.getElementById('btn-market-login')?.addEventListener('click', () => this._showMarketLogin());
document.getElementById('btn-market-tab')?.addEventListener('click', () => {
document.getElementById('dash-category-list').querySelectorAll('.category-item').forEach(el => el.classList.remove('active'));
document.getElementById('btn-market-tab').classList.add('active');
this._marketMode = true;
document.getElementById('dash-search-box').style.display = 'none';
this._loadThemes();
});
this._updateMarketUI();
},
async _installFromMarket(id, filename) {
try {
const res = await fetch(`/api/market/install?id=${id}&filename=${filename}`);
const data = await res.json();
if (data.ok) { Toast.show('安装成功', 'success'); this._loadThemes(); }
else Toast.show('安装失败', 'error');
} catch (e) { Toast.show('安装失败', 'error'); }
},
_showMarketUpload() {
if (!this._marketToken) {
Toast.show('请先登录市场账户', 'info');
this._showMarketLogin();
return;
}
const input = document.createElement('input'); input.type = 'file'; input.accept = '.tsd';
input.onchange = async (e) => {
const file = e.target.files[0]; if (!file) return;
const fd = new FormData(); fd.append('file', file);
fd.append('name', file.name.replace('.tsd',''));
fd.append('category', 'community');
fd.append('author', 'TurboSu User');
fd.append('token', this._marketToken);
try {
const res = await fetch('/api/market/upload', { method: 'POST', body: fd });
const data = await res.json();
Toast.show(data.error ? '上传失败: ' + data.error : '上传成功', data.error ? 'error' : 'success');
} catch (e) { Toast.show('上传失败', 'error'); }
}; input.click();
},
_showMarketLogin() {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = ``;
document.body.appendChild(overlay);
if (this._marketToken) {
overlay.querySelector('#ml-login').style.display = 'none';
overlay.querySelector('#ml-register').style.display = 'none';
overlay.querySelector('#ml-logout').style.display = '';
overlay.querySelector('#ml-logout').addEventListener('click', () => {
this._marketToken = null;
localStorage.removeItem('turbosu-market-token');
overlay.remove();
this._updateMarketUI();
Toast.show('已退出', 'info');
});
}
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
overlay.querySelector('#ml-login').addEventListener('click', async () => {
const email = document.getElementById('ml-email').value;
const pass = document.getElementById('ml-pass').value;
const res = await fetch('/api/market/auth', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({email, password}) });
const data = await res.json();
if (data.token) {
this._marketToken = data.token;
localStorage.setItem('turbosu-market-token', data.token);
Toast.show('登录成功', 'success');
overlay.remove();
this._updateMarketUI();
} else { Toast.show(data.message || '登录失败', 'error'); }
});
overlay.querySelector('#ml-register').addEventListener('click', async () => {
const email = document.getElementById('ml-email').value;
const pass = document.getElementById('ml-pass').value;
const res = await fetch('/api/market/register', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({email, password: pass, passwordConfirm: pass}) });
const data = await res.json();
Toast.show(data.error ? '注册失败: ' + data.error : '注册成功,请查收验证邮件', data.error ? 'error' : 'success');
});
},
_updateMarketUI() {
const loginBtn = document.getElementById('btn-market-login');
const uploadBtn = document.getElementById('btn-market-upload');
if (!loginBtn || !uploadBtn) return;
if (this._marketToken) {
loginBtn.textContent = '已登录';
loginBtn.className = 'btn btn-sm btn-secondary';
uploadBtn.style.display = '';
} else {
loginBtn.textContent = '登录';
loginBtn.className = 'btn btn-sm btn-primary';
uploadBtn.style.display = 'none';
}
},
_openDashboard(themeId) {
const gameParam = this._selectedGameId ? `?game=${this._selectedGameId}` : '';
const url = `${location.origin}/dashboard/${themeId}${gameParam}`;
window.open(url, '_blank');
navigator.clipboard.writeText(url).then(() => {
Toast.show('链接已复制,可在局域网设备访问', 'success');
}).catch(() => {
Toast.show(`打开地址: ${url}`, 'info');
});
},
_exportDashboard(themeId) {
const a = document.createElement('a');
a.href = `/api/dashboards/${themeId}/export`;
a.download = `${themeId}.tsd`;
a.click();
Toast.show('正在下载 .tsd 文件...', 'info');
},
_themeCardHtml(theme) {
const icon = theme.icon || '📊';
const aspect = theme.aspect_ratio || 'auto';
const supported = theme.supported_games || 'all';
const isAll = supported === 'all';
const compatLabel = isAll ? '全' : '限';
const compatClass = isAll ? 'badge-info' : 'badge-warning';
return `
${icon}
${theme.name || 'Unnamed'}
${theme.category || 'basic'}
${aspect}
${theme.author || ''}
${compatLabel}
`;
},
async _createNew() {
const name = prompt('仪表盘名称:', 'My Dashboard');
if (!name) return;
try {
const html = 'TurboSu - ' + name + '