const PageMarket = {
_el: null,
_marketToken: null,
_tab: 'dashboards',
init() {
this._el = document.getElementById('page-market');
this._marketToken = localStorage.getItem('turbosu-market-token');
},
async render() {
this._el.innerHTML = this._template();
this._bindEvents();
this._updateUI();
this._loadItems();
},
async _loadItems() {
const grid = document.getElementById('market-grid');
if (!grid) return;
grid.innerHTML = '
加载中...
';
if (this._tab === 'mine') {
if (!this._marketToken) {
grid.innerHTML = this._emptyHtml('请先登录');
return;
}
return this._loadMine(grid);
}
const isScene = this._tab === 'scenes';
const endpoint = isScene ? '/api/market/scenes' : '/api/market/dashboards';
try {
const res = await fetch(endpoint);
const data = await res.json();
const items = data.items || [];
let localIds = [];
try {
const lr = await fetch('/api/dashboards?category=all');
const ld = await lr.json();
if (Array.isArray(ld)) localIds = ld.map(d => d.id);
if (isScene) {
const sr = await fetch('/api/scenes');
const sl = await sr.json();
if (Array.isArray(sl)) localIds = localIds.concat(sl.map(s => s.id));
}
} catch(e) {}
if (!items.length) { grid.innerHTML = this._emptyHtml(isScene ? '暂无场景' : '暂无仪表盘'); return; }
grid.innerHTML = items.map(d => {
const installed = localIds.includes(d.manifest_id || d.name) || localNames.includes(d.name);
return this._cardHtml(d, isScene, installed);
}).join('');
} catch (e) { console.error(e); grid.innerHTML = '加载失败: ' + (e.message||'未知') + '
'; }
},
async _loadMine(grid) {
try {
const res = await fetch('/api/market/mine?token=' + encodeURIComponent(this._marketToken));
const data = await res.json();
if (!data.dashboards?.length && !data.scenes?.length) {
grid.innerHTML = this._emptyHtml('你还没有上传任何内容');
return;
}
let html = '';
if (data.dashboards?.length) {
html += '我的仪表盘
' + data.dashboards.map(d => this._myCardHtml(d, 'dashboards')).join('');
}
if (data.scenes?.length) {
html += '我的场景
' + data.scenes.map(d => this._myCardHtml(d, 'scenes')).join('');
}
grid.innerHTML = html;
} catch (e) { console.error(e); grid.innerHTML = '加载失败: ' + (e.message||'未知') + '
'; }
},
_cardHtml(d, isScene, installed) {
const icon = isScene ? '🎬' : (d.icon || '📦');
const label = installed ? '已安装' : (isScene ? '安装场景' : '安装');
const btnClass = isScene ? 'btn-m-install-scene' : 'btn-m-install';
const uninstallBtn = installed ? `` : '';
return `
${icon}
${d.name||'Unnamed'}
${isScene ? (d.game_id||'') : (d.category||'')}${isScene ? '' : 'v'+(d.version||'1.0')}
${d.author||''}${d.downloads||0} 次
${uninstallBtn}
`;
},
_myCardHtml(d, collection) {
const pending = d.dashb_pending ? '待审核' : '已通过';
const created = d.created ? new Date(d.created).toLocaleString('zh-CN', {month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '-';
const updated = d.updated ? new Date(d.updated).toLocaleString('zh-CN', {month:'numeric',day:'numeric',hour:'2-digit',minute:'2-digit'}) : created;
return `
${d.name||'Unnamed'} v${d.version||'1.0'}
${d.author||''} · ${d.downloads||0} 次下载 · ${pending}
上传: ${created}${updated!==created?' · 更新: '+updated:''}
`;
},
_bindEvents() {
document.getElementById('btn-m-login')?.addEventListener('click', () => this._showLogin());
document.getElementById('btn-m-upload')?.addEventListener('click', () => this._upload());
document.getElementById('market-grid')?.addEventListener('click', async (e) => {
const btnInstall = e.target.closest('.btn-m-install');
const btnScene = e.target.closest('.btn-m-install-scene');
const btnUpdate = e.target.closest('.btn-my-update');
const btnDelete = e.target.closest('.btn-my-delete');
if (e.target.closest('.btn-m-uninstall')) {
const ubtn = e.target.closest('.btn-m-uninstall');
const uname = ubtn.dataset.name||'';
if (!confirm('确定卸载 "' + uname + '"?')) return;
try {
const r = await fetch('/api/dashboards?category=all');
const list = (await r.json()) || [];
const dash = list.find(d => d.id === uname || d.name === uname);
if (dash) {
await fetch('/api/dashboards/' + dash.id, { method: 'DELETE' });
Toast.show('已卸载', 'success');
this._loadItems();
} else { Toast.show('未找到本地安装', 'error'); }
} catch (e) { Toast.show('卸载失败', 'error'); }
return;
}
if (btnUpdate) {
const input = document.createElement('input'); input.type = 'file'; input.accept = btnUpdate.dataset.type;
input.onchange = async (ev) => {
const file = ev.target.files[0]; if (!file) return;
const fd = new FormData(); fd.append('file', file); fd.append('token', this._marketToken);
const url = btnUpdate.dataset.coll === 'scenes' ? '/api/market/scenes/upload' : '/api/market/upload';
const res = await fetch(url, { method: 'POST', body: fd });
const data = await res.json();
Toast.show(data.error ? '更新失败: ' + data.error : '上传成功(新记录)', data.error ? 'error' : 'success');
if (!data.error) this._loadItems();
}; input.click();
return;
}
if (btnDelete) {
if (!confirm('确定删除?')) return;
try {
await fetch('/api/market/delete', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({id: btnDelete.dataset.id, collection: btnDelete.dataset.coll, token: this._marketToken}) });
Toast.show('已删除', 'info');
this._loadItems();
} catch (e) { Toast.show('删除失败', 'error'); }
return;
}
const id = (btnInstall || btnScene)?.dataset.id;
const file = (btnInstall || btnScene)?.dataset.file;
if (!id || !file) { Toast.show('安装失败:数据不完整', 'error'); return; }
const isScene = !!btnScene;
const url = isScene ? `/api/market/scenes/install?id=${encodeURIComponent(id)}&filename=${encodeURIComponent(file)}`
: `/api/market/install?id=${encodeURIComponent(id)}&filename=${encodeURIComponent(file)}`;
try {
const res = await fetch(url);
const data = await res.json();
if (data.ok) {
Toast.show('安装成功!请在仪表盘页面查看', 'success', 4000);
this._loadItems();
} else {
Toast.show('安装失败: ' + (data.error||'请求异常'), 'error');
}
} catch (e) { Toast.show('安装失败:网络错误', 'error'); }
});
document.getElementById('tab-dashboards')?.addEventListener('click', () => {
this._tab = 'dashboards';
document.getElementById('tab-dashboards').style.cssText = 'border-radius:0;border-bottom:2px solid var(--accent);color:var(--accent);background:transparent;padding:8px 16px';
document.getElementById('tab-scenes').style.cssText = 'border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px';
document.getElementById('tab-mine').style.cssText = 'border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px';
this._loadItems();
});
document.getElementById('tab-scenes')?.addEventListener('click', () => {
this._tab = 'scenes';
document.getElementById('tab-scenes').style.cssText = 'border-radius:0;border-bottom:2px solid var(--accent);color:var(--accent);background:transparent;padding:8px 16px';
document.getElementById('tab-dashboards').style.cssText = 'border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px';
document.getElementById('tab-mine').style.cssText = 'border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px';
this._loadItems();
});
document.getElementById('tab-mine')?.addEventListener('click', () => {
this._tab = 'mine';
document.getElementById('tab-mine').style.cssText = 'border-radius:0;border-bottom:2px solid var(--accent);color:var(--accent);background:transparent;padding:8px 16px';
document.getElementById('tab-dashboards').style.cssText = 'border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px';
document.getElementById('tab-scenes').style.cssText = 'border-radius:0;border-bottom:2px solid transparent;color:var(--text-secondary);background:transparent;padding:8px 16px';
this._loadItems();
});
},
_updateUI() {
const loginBtn = document.getElementById('btn-m-login');
const uploadBtn = document.getElementById('btn-m-upload');
const statusEl = document.getElementById('m-status');
if (!loginBtn || !uploadBtn) return;
if (this._marketToken) {
loginBtn.textContent = '已登录';
loginBtn.className = 'btn btn-sm btn-secondary';
uploadBtn.style.display = '';
if (statusEl) statusEl.textContent = ' 已登录';
} else {
loginBtn.textContent = '登录';
loginBtn.className = 'btn btn-sm btn-primary';
uploadBtn.style.display = 'none';
if (statusEl) statusEl.textContent = ' 未登录';
}
},
_upload() {
const input = document.createElement('input'); input.type = 'file'; input.accept = this._tab === 'scenes' ? '.tss' : '.tsd';
const endpoint = this._tab === 'scenes' ? '/api/market/scenes/upload' : '/api/market/upload';
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');
if (!data.error) this._loadItems();
} catch (e) { Toast.show('上传失败', 'error'); }
}; input.click();
},
_showLogin() {
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._updateUI();
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;
if (!email || !pass) { Toast.show('请输入邮箱和密码', 'error'); return; }
const res = await fetch('/api/market/auth', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({email, password: pass}) });
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._updateUI();
} 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 name = document.getElementById('ml-name')?.value || '';
if (!email || !pass) { Toast.show('请输入邮箱和密码', 'error'); return; }
const res = await fetch('/api/market/register', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({email, password: pass, passwordConfirm: pass, name}) });
const data = await res.json();
Toast.show(data.error ? '注册失败' : '注册成功,请查收验证邮件(点击关闭)', data.error ? 'error' : 'success', data.error ? 3000 : 0);
if (!data.error) overlay.remove();
});
},
_emptyHtml(msg) {
return `${msg||'暂无'}
`;
},
_template() {
return `
仪表盘市场
浏览并安装社区分享的仪表盘和场景 未登录
`;
}
};
window.PageMarket = PageMarket;