Files
TurboSu/static/js/pages/dashboard.js
T
AskaEth a08120c568 feat: 2,3,7,8,9 - search/sort, Docker, log viewer, .tsb backup
- Dashboard browser: search box + sort dropdown (name/category/author)
- Dockerfile + docker-compose.yml
- Debug page: log viewer button (/api/logs)
- Settings: .tsb full backup export (dashboards+scenes+config)
- F1 composite dashboard previously included
2026-07-26 17:32:09 +08:00

217 lines
8.7 KiB
JavaScript

const PageDashboard = {
_el: null,
_currentCategory: 'all',
_selectedGameId: null,
init() {
this._el = document.getElementById('page-dashboard');
},
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 = '<div style="text-align:center;padding:40px;color:var(--text-tertiary);">加载中...</div>';
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) {
console.error('Failed to load themes:', e);
gridEl.innerHTML = '<div style="text-align:center;padding:40px;color:var(--danger);">加载失败</div>';
}
},
_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 `
<div class="empty-state" style="grid-column:1/-1;">
<svg viewBox="0 0 24 24" width="64" height="64"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-12c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z" fill="currentColor"/></svg>
<h4>${msg || '暂无仪表盘主题'}</h4>
<p>创建或导入你的仪表盘主题</p>
</div>`;
},
_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;
this._openDashboard(themeId);
});
gridEl.addEventListener('contextmenu', (e) => {
const card = e.target.closest('.theme-card');
if (!card) return;
e.preventDefault();
const themeId = card.dataset.themeId;
this._exportDashboard(themeId);
});
}
document.getElementById('dash-search')?.addEventListener('input', () => this._loadThemes());
document.getElementById('dash-sort')?.addEventListener('change', () => this._loadThemes());
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 formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/dashboards/import', { method: 'POST', body: formData });
const data = await res.json();
if (data.ok) { Toast.show('仪表盘导入成功', 'success'); this._loadThemes(); }
else Toast.show('导入失败', 'error');
} catch (err) { Toast.show('导入失败', 'error'); }
};
input.click();
});
},
_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 `
<div class="glass-card theme-card" data-theme-id="${theme.id}">
<div class="theme-card-preview">${icon}</div>
<div class="theme-card-info">
<div class="theme-card-name">${theme.name || 'Unnamed'}</div>
<div class="theme-card-meta">
<span>${theme.category || 'basic'}</span>
<span>${aspect}</span>
</div>
<div class="theme-card-meta" style="margin-top:4px;">
<span>${theme.author || ''}</span>
<span class="badge ${compatClass}">${compatLabel}</span>
</div>
</div>
</div>`;
},
_template() {
return `
<div class="dashboard-layout">
<div class="dashboard-sub-sidebar">
<div id="dash-category-list"></div>
<div style="padding:8px 12px;margin-top:auto;">
<button id="btn-import-dashboard" class="btn btn-sm btn-secondary" style="width:100%;">导入 .tsd</button>
</div>
</div>
<div class="dashboard-main">
<div style="display:flex;gap:8px;margin-bottom:12px;align-items:center">
<input id="dash-search" type="text" placeholder="搜索仪表盘..." style="flex:1;max-width:300px">
<select id="dash-sort" style="width:120px">
<option value="name">名称</option>
<option value="category">分类</option>
<option value="author">作者</option>
</select>
</div>
<div id="dash-theme-grid" class="theme-grid"></div>
</div>
</div>`;
}
};
window.PageDashboard = PageDashboard;